@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/llms.txt
CHANGED
|
@@ -44,6 +44,12 @@ import { Created, NoContent } from '@web-ts-toolkit/express-response-handler/res
|
|
|
44
44
|
- default export is the handler instance; named exports (`handleResponse`, `HttpResponse`, `createHandler`, `ErrorFormats`) are also available
|
|
45
45
|
- return plain values for `200 OK`; use `HttpResponse.*` wrappers for other status codes
|
|
46
46
|
- `ErrorFormats` selects between `simple`, `aip193`, and `rfc9457` error payload shapes
|
|
47
|
+
- unexpected non-HTTP errors return `500` with `Internal Server Error`; raw thrown messages are kept for hooks/logging and are not sent to clients by default
|
|
48
|
+
- only finite integer `4xx` and `5xx` status codes are serialized as HTTP errors; invalid status values are rejected before headers are written
|
|
49
|
+
- hooks (`preJson`, `postJson`, `preError`, `postError`) are observational `void | Promise<void>` side effects; returned values never transform payloads
|
|
50
|
+
- `postJson` and `postError` run after successful HTTP `finish`; they do not run on client `close`, failed serialization, or `undefined` handler returns
|
|
51
|
+
- pre-hook failures go through the normal error response path; post-hook failures are passed to Express with `next(err)` after the response has finished
|
|
52
|
+
- the default export is a mutable process-wide singleton; use `createHandler()` for isolated hook and error-provider state
|
|
47
53
|
- response wrappers (`Created`, `NoContent`, `CSVResponse`, etc.) are also exported from the root entrypoint, but the subpaths give tree-shakeable access
|
|
48
54
|
|
|
49
55
|
## Pointers
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@web-ts-toolkit/express-response-handler",
|
|
3
3
|
"description": "FastAPI-style return-value response handling for Express",
|
|
4
4
|
"homepage": "https://web-ts-toolkit.pages.dev/docs/packages/express-response-handler",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.34.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"express",
|
|
@@ -47,12 +47,16 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
|
-
"node": ">=
|
|
50
|
+
"node": ">=22"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@fast-csv/format": "^5.0.7",
|
|
54
|
-
"@
|
|
55
|
-
"@web-ts-toolkit/
|
|
54
|
+
"@types/express-serve-static-core": "^5.1.1",
|
|
55
|
+
"@web-ts-toolkit/http-errors": "0.34.0",
|
|
56
|
+
"@web-ts-toolkit/utils": "0.34.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"express": "^5.0.0"
|
|
56
60
|
},
|
|
57
61
|
"files": [
|
|
58
62
|
"**/*",
|
package/public-types.d.mts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import { ParamsDictionary, Query, Request, Response, NextFunction as NextFunction$1, RequestHandler } from 'express-serve-static-core';
|
|
1
2
|
import { HttpResponseHelpers } from './http-response.mjs';
|
|
2
3
|
import { ErrorFormats } from './error-formats.mjs';
|
|
4
|
+
import './responses/csv.mjs';
|
|
3
5
|
import '@web-ts-toolkit/http-errors';
|
|
4
6
|
import './responses/success.mjs';
|
|
5
7
|
import './responses/index.mjs';
|
|
6
|
-
import './responses/csv.mjs';
|
|
7
8
|
|
|
8
9
|
type ErrorMessageResult = string | Record<string, unknown>;
|
|
9
10
|
type ErrorMessageProvider = (error: unknown) => ErrorMessageResult;
|
|
10
11
|
type ErrorFormat = (typeof ErrorFormats)[keyof typeof ErrorFormats];
|
|
11
|
-
type MaybePromise<T> = T |
|
|
12
|
-
type Hook = (value: unknown) =>
|
|
13
|
-
type AsyncHook = (value: unknown) => Promise<
|
|
14
|
-
type NextFunction =
|
|
12
|
+
type MaybePromise<T> = T | PromiseLike<T>;
|
|
13
|
+
type Hook = (value: unknown) => void | PromiseLike<void>;
|
|
14
|
+
type AsyncHook = (value: unknown) => Promise<void>;
|
|
15
|
+
type NextFunction = NextFunction$1;
|
|
15
16
|
type ExpressResponseHandlerOptions = {
|
|
16
17
|
errorFormat?: ErrorFormat;
|
|
17
18
|
errorDomain?: string;
|
|
@@ -23,18 +24,15 @@ type ResponseLike = {
|
|
|
23
24
|
json(data: unknown): unknown;
|
|
24
25
|
send(data: unknown): unknown;
|
|
25
26
|
set(name: string, value: string): unknown;
|
|
27
|
+
once?(event: 'finish', listener: () => void): unknown;
|
|
26
28
|
end(): void;
|
|
27
29
|
};
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
nextError: unknown;
|
|
31
|
-
};
|
|
32
|
-
type MiddlewareFunction<Request = unknown, Response extends ResponseLike = ResponseLike, Return = unknown> = (req: Request, res: Response, next: NextFunction) => MaybePromise<Return>;
|
|
33
|
-
type RouterFunction<Request = unknown, Response extends ResponseLike = ResponseLike> = (req: Request, res: Response, next: NextFunction) => void;
|
|
30
|
+
type MiddlewareFunction<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown> = (req: Request<Params, ResBody, ReqBody, ReqQuery, Locals>, res: Response<ResBody, Locals>, next: NextFunction) => MaybePromise<Return>;
|
|
31
|
+
type RouterFunction<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>> = RequestHandler<Params, ResBody, ReqBody, ReqQuery, Locals>;
|
|
34
32
|
type HandleResponse = {
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
<
|
|
33
|
+
<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown>(fn: MiddlewareFunction<Params, ResBody, ReqBody, ReqQuery, Locals, Return>): RouterFunction<Params, ResBody, ReqBody, ReqQuery, Locals>;
|
|
34
|
+
<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown>(...fns: Array<MiddlewareFunction<Params, ResBody, ReqBody, ReqQuery, Locals, Return>>): Array<RouterFunction<Params, ResBody, ReqBody, ReqQuery, Locals>>;
|
|
35
|
+
<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown>(fns: Array<MiddlewareFunction<Params, ResBody, ReqBody, ReqQuery, Locals, Return>>): Array<RouterFunction<Params, ResBody, ReqBody, ReqQuery, Locals>>;
|
|
38
36
|
};
|
|
39
37
|
type ErrorWithPayload = {
|
|
40
38
|
statusCode?: number;
|
|
@@ -53,8 +51,6 @@ type ErrorWithPayload = {
|
|
|
53
51
|
type CreateHandler = (options?: ExpressResponseHandlerOptions) => ExpressResponseHandler;
|
|
54
52
|
type ExpressResponseHandler = {
|
|
55
53
|
handleResponse: HandleResponse;
|
|
56
|
-
handleResult: (res: ResponseLike, result: unknown, event: EventState) => void;
|
|
57
|
-
handlePromise: (res: ResponseLike, promise: Promise<unknown>, event: EventState) => void;
|
|
58
54
|
HttpResponse: HttpResponseHelpers;
|
|
59
55
|
createHandler: CreateHandler;
|
|
60
56
|
errorMessageProvider: ErrorMessageProvider;
|
|
@@ -64,4 +60,4 @@ type ExpressResponseHandler = {
|
|
|
64
60
|
postError: Hook | null;
|
|
65
61
|
};
|
|
66
62
|
|
|
67
|
-
export { type AsyncHook, type CreateHandler, type ErrorFormat, ErrorFormats, type ErrorMessageProvider, type ErrorMessageResult, type ErrorWithPayload, type
|
|
63
|
+
export { type AsyncHook, type CreateHandler, type ErrorFormat, ErrorFormats, type ErrorMessageProvider, type ErrorMessageResult, type ErrorWithPayload, type ExpressResponseHandler, type ExpressResponseHandlerOptions, type HandleResponse, type Hook, HttpResponseHelpers, type MaybePromise, type MiddlewareFunction, type NextFunction, type ResponseLike, type RouterFunction };
|
package/public-types.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import { ParamsDictionary, Query, Request, Response, NextFunction as NextFunction$1, RequestHandler } from 'express-serve-static-core';
|
|
1
2
|
import { HttpResponseHelpers } from './http-response.js';
|
|
2
3
|
import { ErrorFormats } from './error-formats.js';
|
|
4
|
+
import './responses/csv.js';
|
|
3
5
|
import '@web-ts-toolkit/http-errors';
|
|
4
6
|
import './responses/success.js';
|
|
5
7
|
import './responses/index.js';
|
|
6
|
-
import './responses/csv.js';
|
|
7
8
|
|
|
8
9
|
type ErrorMessageResult = string | Record<string, unknown>;
|
|
9
10
|
type ErrorMessageProvider = (error: unknown) => ErrorMessageResult;
|
|
10
11
|
type ErrorFormat = (typeof ErrorFormats)[keyof typeof ErrorFormats];
|
|
11
|
-
type MaybePromise<T> = T |
|
|
12
|
-
type Hook = (value: unknown) =>
|
|
13
|
-
type AsyncHook = (value: unknown) => Promise<
|
|
14
|
-
type NextFunction =
|
|
12
|
+
type MaybePromise<T> = T | PromiseLike<T>;
|
|
13
|
+
type Hook = (value: unknown) => void | PromiseLike<void>;
|
|
14
|
+
type AsyncHook = (value: unknown) => Promise<void>;
|
|
15
|
+
type NextFunction = NextFunction$1;
|
|
15
16
|
type ExpressResponseHandlerOptions = {
|
|
16
17
|
errorFormat?: ErrorFormat;
|
|
17
18
|
errorDomain?: string;
|
|
@@ -23,18 +24,15 @@ type ResponseLike = {
|
|
|
23
24
|
json(data: unknown): unknown;
|
|
24
25
|
send(data: unknown): unknown;
|
|
25
26
|
set(name: string, value: string): unknown;
|
|
27
|
+
once?(event: 'finish', listener: () => void): unknown;
|
|
26
28
|
end(): void;
|
|
27
29
|
};
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
nextError: unknown;
|
|
31
|
-
};
|
|
32
|
-
type MiddlewareFunction<Request = unknown, Response extends ResponseLike = ResponseLike, Return = unknown> = (req: Request, res: Response, next: NextFunction) => MaybePromise<Return>;
|
|
33
|
-
type RouterFunction<Request = unknown, Response extends ResponseLike = ResponseLike> = (req: Request, res: Response, next: NextFunction) => void;
|
|
30
|
+
type MiddlewareFunction<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown> = (req: Request<Params, ResBody, ReqBody, ReqQuery, Locals>, res: Response<ResBody, Locals>, next: NextFunction) => MaybePromise<Return>;
|
|
31
|
+
type RouterFunction<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>> = RequestHandler<Params, ResBody, ReqBody, ReqQuery, Locals>;
|
|
34
32
|
type HandleResponse = {
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
<
|
|
33
|
+
<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown>(fn: MiddlewareFunction<Params, ResBody, ReqBody, ReqQuery, Locals, Return>): RouterFunction<Params, ResBody, ReqBody, ReqQuery, Locals>;
|
|
34
|
+
<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown>(...fns: Array<MiddlewareFunction<Params, ResBody, ReqBody, ReqQuery, Locals, Return>>): Array<RouterFunction<Params, ResBody, ReqBody, ReqQuery, Locals>>;
|
|
35
|
+
<Params = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>, Return = unknown>(fns: Array<MiddlewareFunction<Params, ResBody, ReqBody, ReqQuery, Locals, Return>>): Array<RouterFunction<Params, ResBody, ReqBody, ReqQuery, Locals>>;
|
|
38
36
|
};
|
|
39
37
|
type ErrorWithPayload = {
|
|
40
38
|
statusCode?: number;
|
|
@@ -53,8 +51,6 @@ type ErrorWithPayload = {
|
|
|
53
51
|
type CreateHandler = (options?: ExpressResponseHandlerOptions) => ExpressResponseHandler;
|
|
54
52
|
type ExpressResponseHandler = {
|
|
55
53
|
handleResponse: HandleResponse;
|
|
56
|
-
handleResult: (res: ResponseLike, result: unknown, event: EventState) => void;
|
|
57
|
-
handlePromise: (res: ResponseLike, promise: Promise<unknown>, event: EventState) => void;
|
|
58
54
|
HttpResponse: HttpResponseHelpers;
|
|
59
55
|
createHandler: CreateHandler;
|
|
60
56
|
errorMessageProvider: ErrorMessageProvider;
|
|
@@ -64,4 +60,4 @@ type ExpressResponseHandler = {
|
|
|
64
60
|
postError: Hook | null;
|
|
65
61
|
};
|
|
66
62
|
|
|
67
|
-
export { type AsyncHook, type CreateHandler, type ErrorFormat, ErrorFormats, type ErrorMessageProvider, type ErrorMessageResult, type ErrorWithPayload, type
|
|
63
|
+
export { type AsyncHook, type CreateHandler, type ErrorFormat, ErrorFormats, type ErrorMessageProvider, type ErrorMessageResult, type ErrorWithPayload, type ExpressResponseHandler, type ExpressResponseHandlerOptions, type HandleResponse, type Hook, HttpResponseHelpers, type MaybePromise, type MiddlewareFunction, type NextFunction, type ResponseLike, type RouterFunction };
|
package/responses/csv.d.mts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
type CsvProcessor = (value: unknown) => unknown;
|
|
2
|
+
type CsvErrorHandler = (error: unknown) => void;
|
|
3
|
+
type CsvHeaders = boolean | string[];
|
|
4
|
+
type CsvSource = unknown[] | Iterable<unknown> | AsyncIterable<unknown> | unknown;
|
|
2
5
|
type CsvResponseOptions = {
|
|
3
6
|
filename?: string;
|
|
4
|
-
headers?:
|
|
7
|
+
headers?: CsvHeaders;
|
|
5
8
|
processor?: CsvProcessor;
|
|
6
9
|
};
|
|
7
10
|
type CsvStreamResponse = {
|
|
11
|
+
headersSent?: boolean;
|
|
8
12
|
set(name: string, value: string): unknown;
|
|
9
13
|
destroy?(error?: Error): void;
|
|
10
14
|
end(): void;
|
|
15
|
+
on?(event: 'error' | 'close' | 'finish', listener: (...args: unknown[]) => void): unknown;
|
|
16
|
+
off?(event: 'error' | 'close' | 'finish', listener: (...args: unknown[]) => void): unknown;
|
|
11
17
|
};
|
|
12
18
|
declare const csvResponseBrand: unique symbol;
|
|
13
19
|
declare const isCSVResponse: (value: unknown) => value is CSVResponse;
|
|
14
20
|
declare class CSVResponse {
|
|
15
21
|
readonly [csvResponseBrand] = true;
|
|
16
|
-
readonly dataset:
|
|
22
|
+
readonly dataset: CsvSource;
|
|
17
23
|
readonly filename: string;
|
|
18
24
|
readonly processor: CsvProcessor;
|
|
19
|
-
readonly headers?:
|
|
20
|
-
constructor(dataset?:
|
|
21
|
-
streamCsv(res: CsvStreamResponse): void;
|
|
25
|
+
readonly headers?: CsvHeaders;
|
|
26
|
+
constructor(dataset?: CsvSource, options?: CsvResponseOptions);
|
|
27
|
+
streamCsv(res: CsvStreamResponse, onBeforeOutputError?: CsvErrorHandler): void;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export { CSVResponse, csvResponseBrand, isCSVResponse };
|
|
30
|
+
export { CSVResponse, type CsvResponseOptions, type CsvSource, csvResponseBrand, isCSVResponse };
|
package/responses/csv.d.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
type CsvProcessor = (value: unknown) => unknown;
|
|
2
|
+
type CsvErrorHandler = (error: unknown) => void;
|
|
3
|
+
type CsvHeaders = boolean | string[];
|
|
4
|
+
type CsvSource = unknown[] | Iterable<unknown> | AsyncIterable<unknown> | unknown;
|
|
2
5
|
type CsvResponseOptions = {
|
|
3
6
|
filename?: string;
|
|
4
|
-
headers?:
|
|
7
|
+
headers?: CsvHeaders;
|
|
5
8
|
processor?: CsvProcessor;
|
|
6
9
|
};
|
|
7
10
|
type CsvStreamResponse = {
|
|
11
|
+
headersSent?: boolean;
|
|
8
12
|
set(name: string, value: string): unknown;
|
|
9
13
|
destroy?(error?: Error): void;
|
|
10
14
|
end(): void;
|
|
15
|
+
on?(event: 'error' | 'close' | 'finish', listener: (...args: unknown[]) => void): unknown;
|
|
16
|
+
off?(event: 'error' | 'close' | 'finish', listener: (...args: unknown[]) => void): unknown;
|
|
11
17
|
};
|
|
12
18
|
declare const csvResponseBrand: unique symbol;
|
|
13
19
|
declare const isCSVResponse: (value: unknown) => value is CSVResponse;
|
|
14
20
|
declare class CSVResponse {
|
|
15
21
|
readonly [csvResponseBrand] = true;
|
|
16
|
-
readonly dataset:
|
|
22
|
+
readonly dataset: CsvSource;
|
|
17
23
|
readonly filename: string;
|
|
18
24
|
readonly processor: CsvProcessor;
|
|
19
|
-
readonly headers?:
|
|
20
|
-
constructor(dataset?:
|
|
21
|
-
streamCsv(res: CsvStreamResponse): void;
|
|
25
|
+
readonly headers?: CsvHeaders;
|
|
26
|
+
constructor(dataset?: CsvSource, options?: CsvResponseOptions);
|
|
27
|
+
streamCsv(res: CsvStreamResponse, onBeforeOutputError?: CsvErrorHandler): void;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export { CSVResponse, csvResponseBrand, isCSVResponse };
|
|
30
|
+
export { CSVResponse, type CsvResponseOptions, type CsvSource, csvResponseBrand, isCSVResponse };
|
package/responses/csv.js
CHANGED
|
@@ -27,6 +27,41 @@ __export(csv_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(csv_exports);
|
|
28
28
|
var import_format = require("@fast-csv/format");
|
|
29
29
|
var import_utils = require("@web-ts-toolkit/utils");
|
|
30
|
+
var import_events = require("events");
|
|
31
|
+
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
32
|
+
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
33
|
+
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
34
|
+
var isLazyCsvSource = (value) => !Array.isArray(value) && (isIterable(value) || isAsyncIterable(value));
|
|
35
|
+
var createAsyncIterator = (source) => {
|
|
36
|
+
if (isAsyncIterable(source)) {
|
|
37
|
+
return source[Symbol.asyncIterator]();
|
|
38
|
+
}
|
|
39
|
+
const iterator = (isIterable(source) ? source : (0, import_utils.castArray)(source))[Symbol.iterator]();
|
|
40
|
+
return {
|
|
41
|
+
next: () => Promise.resolve(iterator.next()),
|
|
42
|
+
return: (value) => {
|
|
43
|
+
if (iterator.return) {
|
|
44
|
+
return Promise.resolve(iterator.return(value));
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve({ done: true, value });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
var encodeRFC5987Value = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
51
|
+
var createQuotedFilenameFallback = (filename) => {
|
|
52
|
+
const fallback = Array.from(filename.replace(/[\\/]/g, "_"), (char) => {
|
|
53
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
54
|
+
return codePoint >= 32 && codePoint <= 126 ? char : "_";
|
|
55
|
+
}).join("");
|
|
56
|
+
return fallback.replace(/(["\\])/g, "\\$1");
|
|
57
|
+
};
|
|
58
|
+
var createAttachmentContentDisposition = (filename) => {
|
|
59
|
+
if (CONTROL_CHARACTER_PATTERN.test(filename)) {
|
|
60
|
+
throw new TypeError("CSV filename cannot contain control characters");
|
|
61
|
+
}
|
|
62
|
+
const fallback = createQuotedFilenameFallback(filename);
|
|
63
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987Value(filename)}`;
|
|
64
|
+
};
|
|
30
65
|
var csvResponseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.csv-response");
|
|
31
66
|
var isCSVResponse = (value) => {
|
|
32
67
|
if (value instanceof CSVResponse) {
|
|
@@ -43,30 +78,116 @@ _a = csvResponseBrand;
|
|
|
43
78
|
var CSVResponse = class {
|
|
44
79
|
constructor(dataset = [], options = {}) {
|
|
45
80
|
this[_a] = true;
|
|
46
|
-
this.dataset =
|
|
81
|
+
this.dataset = dataset;
|
|
47
82
|
this.filename = options.filename || "download.csv";
|
|
48
83
|
this.processor = options.processor || ((value) => value);
|
|
49
|
-
if ((0, import_utils.isBoolean)(options.headers)) {
|
|
84
|
+
if ((0, import_utils.isBoolean)(options.headers) || Array.isArray(options.headers)) {
|
|
50
85
|
this.headers = options.headers;
|
|
51
|
-
} else if (this.dataset.length > 0) {
|
|
86
|
+
} else if (Array.isArray(this.dataset) && this.dataset.length > 0) {
|
|
52
87
|
this.headers = (0, import_utils.isPlainObject)(this.dataset[0]);
|
|
88
|
+
} else if (isLazyCsvSource(this.dataset)) {
|
|
89
|
+
throw new TypeError("CSV lazy sources require an explicit headers option");
|
|
90
|
+
} else {
|
|
91
|
+
this.headers = (0, import_utils.isPlainObject)(this.dataset);
|
|
53
92
|
}
|
|
54
93
|
}
|
|
55
|
-
streamCsv(res) {
|
|
94
|
+
streamCsv(res, onBeforeOutputError) {
|
|
56
95
|
const stream = (0, import_format.format)({ headers: this.headers });
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
96
|
+
let outputStarted = false;
|
|
97
|
+
let failed = false;
|
|
98
|
+
let finished = false;
|
|
99
|
+
let activeIterator = null;
|
|
100
|
+
let abortStreaming = () => void 0;
|
|
101
|
+
const abortSignal = new Promise((_, reject) => {
|
|
102
|
+
abortStreaming = reject;
|
|
103
|
+
});
|
|
104
|
+
abortSignal.catch(() => void 0);
|
|
105
|
+
const cleanup = () => {
|
|
106
|
+
stream.off("error", fail);
|
|
107
|
+
res.off?.("error", fail);
|
|
108
|
+
res.off?.("close", abort);
|
|
109
|
+
res.off?.("finish", markFinished);
|
|
110
|
+
};
|
|
111
|
+
const closeActiveIterator = () => {
|
|
112
|
+
const iterator = activeIterator;
|
|
113
|
+
activeIterator = null;
|
|
114
|
+
if (iterator?.return) {
|
|
115
|
+
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const closeResponse = (error) => {
|
|
61
119
|
if (res.destroy) {
|
|
62
120
|
res.destroy(error);
|
|
63
121
|
return;
|
|
64
122
|
}
|
|
65
123
|
res.end();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
124
|
+
};
|
|
125
|
+
const fail = (error) => {
|
|
126
|
+
if (failed) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
failed = true;
|
|
130
|
+
cleanup();
|
|
131
|
+
const normalizedError = error instanceof Error ? error : new Error("CSV response streaming failed");
|
|
132
|
+
abortStreaming(normalizedError);
|
|
133
|
+
closeActiveIterator();
|
|
134
|
+
if (!outputStarted && !res.headersSent) {
|
|
135
|
+
onBeforeOutputError?.(normalizedError);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
stream.destroy();
|
|
139
|
+
closeResponse(normalizedError);
|
|
140
|
+
};
|
|
141
|
+
const abort = () => {
|
|
142
|
+
if (!finished) {
|
|
143
|
+
fail(new Error("CSV response streaming aborted"));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const markFinished = () => {
|
|
147
|
+
finished = true;
|
|
148
|
+
};
|
|
149
|
+
const writeRow = async (row) => {
|
|
150
|
+
outputStarted = true;
|
|
151
|
+
if (!stream.write(row)) {
|
|
152
|
+
await Promise.race([(0, import_events.once)(stream, "drain"), abortSignal]);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
stream.on("error", fail);
|
|
156
|
+
stream.on("end", cleanup);
|
|
157
|
+
res.on?.("error", fail);
|
|
158
|
+
res.on?.("close", abort);
|
|
159
|
+
res.on?.("finish", markFinished);
|
|
160
|
+
const pump = async () => {
|
|
161
|
+
try {
|
|
162
|
+
const iterator = createAsyncIterator(this.dataset);
|
|
163
|
+
activeIterator = iterator;
|
|
164
|
+
const first = await iterator.next();
|
|
165
|
+
const firstRow = first.done ? void 0 : this.processor(first.value);
|
|
166
|
+
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
167
|
+
res.set("Content-Type", "text/csv");
|
|
168
|
+
res.set("Content-Disposition", contentDisposition);
|
|
169
|
+
stream.pipe(res);
|
|
170
|
+
if (!first.done) {
|
|
171
|
+
await writeRow(firstRow);
|
|
172
|
+
}
|
|
173
|
+
while (!failed) {
|
|
174
|
+
const row = await Promise.race([iterator.next(), abortSignal]);
|
|
175
|
+
if (row.done) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
await writeRow(this.processor(row.value));
|
|
179
|
+
}
|
|
180
|
+
if (!failed) {
|
|
181
|
+
finished = true;
|
|
182
|
+
stream.end();
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
fail(error);
|
|
186
|
+
} finally {
|
|
187
|
+
activeIterator = null;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
void pump();
|
|
70
191
|
}
|
|
71
192
|
};
|
|
72
193
|
// Annotate the CommonJS export names for ESM import in node:
|
package/responses/csv.mjs
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
// src/responses/csv.ts
|
|
2
2
|
import { format } from "@fast-csv/format";
|
|
3
3
|
import { castArray, isBoolean, isPlainObject } from "@web-ts-toolkit/utils";
|
|
4
|
+
import { once } from "events";
|
|
5
|
+
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
6
|
+
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
7
|
+
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
8
|
+
var isLazyCsvSource = (value) => !Array.isArray(value) && (isIterable(value) || isAsyncIterable(value));
|
|
9
|
+
var createAsyncIterator = (source) => {
|
|
10
|
+
if (isAsyncIterable(source)) {
|
|
11
|
+
return source[Symbol.asyncIterator]();
|
|
12
|
+
}
|
|
13
|
+
const iterator = (isIterable(source) ? source : castArray(source))[Symbol.iterator]();
|
|
14
|
+
return {
|
|
15
|
+
next: () => Promise.resolve(iterator.next()),
|
|
16
|
+
return: (value) => {
|
|
17
|
+
if (iterator.return) {
|
|
18
|
+
return Promise.resolve(iterator.return(value));
|
|
19
|
+
}
|
|
20
|
+
return Promise.resolve({ done: true, value });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
var encodeRFC5987Value = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
25
|
+
var createQuotedFilenameFallback = (filename) => {
|
|
26
|
+
const fallback = Array.from(filename.replace(/[\\/]/g, "_"), (char) => {
|
|
27
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
28
|
+
return codePoint >= 32 && codePoint <= 126 ? char : "_";
|
|
29
|
+
}).join("");
|
|
30
|
+
return fallback.replace(/(["\\])/g, "\\$1");
|
|
31
|
+
};
|
|
32
|
+
var createAttachmentContentDisposition = (filename) => {
|
|
33
|
+
if (CONTROL_CHARACTER_PATTERN.test(filename)) {
|
|
34
|
+
throw new TypeError("CSV filename cannot contain control characters");
|
|
35
|
+
}
|
|
36
|
+
const fallback = createQuotedFilenameFallback(filename);
|
|
37
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987Value(filename)}`;
|
|
38
|
+
};
|
|
4
39
|
var csvResponseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.csv-response");
|
|
5
40
|
var isCSVResponse = (value) => {
|
|
6
41
|
if (value instanceof CSVResponse) {
|
|
@@ -17,30 +52,116 @@ _a = csvResponseBrand;
|
|
|
17
52
|
var CSVResponse = class {
|
|
18
53
|
constructor(dataset = [], options = {}) {
|
|
19
54
|
this[_a] = true;
|
|
20
|
-
this.dataset =
|
|
55
|
+
this.dataset = dataset;
|
|
21
56
|
this.filename = options.filename || "download.csv";
|
|
22
57
|
this.processor = options.processor || ((value) => value);
|
|
23
|
-
if (isBoolean(options.headers)) {
|
|
58
|
+
if (isBoolean(options.headers) || Array.isArray(options.headers)) {
|
|
24
59
|
this.headers = options.headers;
|
|
25
|
-
} else if (this.dataset.length > 0) {
|
|
60
|
+
} else if (Array.isArray(this.dataset) && this.dataset.length > 0) {
|
|
26
61
|
this.headers = isPlainObject(this.dataset[0]);
|
|
62
|
+
} else if (isLazyCsvSource(this.dataset)) {
|
|
63
|
+
throw new TypeError("CSV lazy sources require an explicit headers option");
|
|
64
|
+
} else {
|
|
65
|
+
this.headers = isPlainObject(this.dataset);
|
|
27
66
|
}
|
|
28
67
|
}
|
|
29
|
-
streamCsv(res) {
|
|
68
|
+
streamCsv(res, onBeforeOutputError) {
|
|
30
69
|
const stream = format({ headers: this.headers });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
70
|
+
let outputStarted = false;
|
|
71
|
+
let failed = false;
|
|
72
|
+
let finished = false;
|
|
73
|
+
let activeIterator = null;
|
|
74
|
+
let abortStreaming = () => void 0;
|
|
75
|
+
const abortSignal = new Promise((_, reject) => {
|
|
76
|
+
abortStreaming = reject;
|
|
77
|
+
});
|
|
78
|
+
abortSignal.catch(() => void 0);
|
|
79
|
+
const cleanup = () => {
|
|
80
|
+
stream.off("error", fail);
|
|
81
|
+
res.off?.("error", fail);
|
|
82
|
+
res.off?.("close", abort);
|
|
83
|
+
res.off?.("finish", markFinished);
|
|
84
|
+
};
|
|
85
|
+
const closeActiveIterator = () => {
|
|
86
|
+
const iterator = activeIterator;
|
|
87
|
+
activeIterator = null;
|
|
88
|
+
if (iterator?.return) {
|
|
89
|
+
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const closeResponse = (error) => {
|
|
35
93
|
if (res.destroy) {
|
|
36
94
|
res.destroy(error);
|
|
37
95
|
return;
|
|
38
96
|
}
|
|
39
97
|
res.end();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
};
|
|
99
|
+
const fail = (error) => {
|
|
100
|
+
if (failed) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
failed = true;
|
|
104
|
+
cleanup();
|
|
105
|
+
const normalizedError = error instanceof Error ? error : new Error("CSV response streaming failed");
|
|
106
|
+
abortStreaming(normalizedError);
|
|
107
|
+
closeActiveIterator();
|
|
108
|
+
if (!outputStarted && !res.headersSent) {
|
|
109
|
+
onBeforeOutputError?.(normalizedError);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
stream.destroy();
|
|
113
|
+
closeResponse(normalizedError);
|
|
114
|
+
};
|
|
115
|
+
const abort = () => {
|
|
116
|
+
if (!finished) {
|
|
117
|
+
fail(new Error("CSV response streaming aborted"));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const markFinished = () => {
|
|
121
|
+
finished = true;
|
|
122
|
+
};
|
|
123
|
+
const writeRow = async (row) => {
|
|
124
|
+
outputStarted = true;
|
|
125
|
+
if (!stream.write(row)) {
|
|
126
|
+
await Promise.race([once(stream, "drain"), abortSignal]);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
stream.on("error", fail);
|
|
130
|
+
stream.on("end", cleanup);
|
|
131
|
+
res.on?.("error", fail);
|
|
132
|
+
res.on?.("close", abort);
|
|
133
|
+
res.on?.("finish", markFinished);
|
|
134
|
+
const pump = async () => {
|
|
135
|
+
try {
|
|
136
|
+
const iterator = createAsyncIterator(this.dataset);
|
|
137
|
+
activeIterator = iterator;
|
|
138
|
+
const first = await iterator.next();
|
|
139
|
+
const firstRow = first.done ? void 0 : this.processor(first.value);
|
|
140
|
+
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
141
|
+
res.set("Content-Type", "text/csv");
|
|
142
|
+
res.set("Content-Disposition", contentDisposition);
|
|
143
|
+
stream.pipe(res);
|
|
144
|
+
if (!first.done) {
|
|
145
|
+
await writeRow(firstRow);
|
|
146
|
+
}
|
|
147
|
+
while (!failed) {
|
|
148
|
+
const row = await Promise.race([iterator.next(), abortSignal]);
|
|
149
|
+
if (row.done) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
await writeRow(this.processor(row.value));
|
|
153
|
+
}
|
|
154
|
+
if (!failed) {
|
|
155
|
+
finished = true;
|
|
156
|
+
stream.end();
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
fail(error);
|
|
160
|
+
} finally {
|
|
161
|
+
activeIterator = null;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
void pump();
|
|
44
165
|
}
|
|
45
166
|
};
|
|
46
167
|
export {
|
package/responses/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
declare const responseBrand: unique symbol;
|
|
2
|
+
declare const validateHttpStatusCode: (statusCode: unknown) => number;
|
|
2
3
|
declare const isResponse: (value: unknown) => value is Response<unknown>;
|
|
3
4
|
declare class Response<T = unknown> {
|
|
4
5
|
readonly [responseBrand] = true;
|
|
@@ -7,4 +8,4 @@ declare class Response<T = unknown> {
|
|
|
7
8
|
constructor(statusCode: number | undefined, data: T);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export { Response, isResponse, responseBrand };
|
|
11
|
+
export { Response, isResponse, responseBrand, validateHttpStatusCode };
|
package/responses/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
declare const responseBrand: unique symbol;
|
|
2
|
+
declare const validateHttpStatusCode: (statusCode: unknown) => number;
|
|
2
3
|
declare const isResponse: (value: unknown) => value is Response<unknown>;
|
|
3
4
|
declare class Response<T = unknown> {
|
|
4
5
|
readonly [responseBrand] = true;
|
|
@@ -7,4 +8,4 @@ declare class Response<T = unknown> {
|
|
|
7
8
|
constructor(statusCode: number | undefined, data: T);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export { Response, isResponse, responseBrand };
|
|
11
|
+
export { Response, isResponse, responseBrand, validateHttpStatusCode };
|
package/responses/index.js
CHANGED
|
@@ -22,10 +22,17 @@ var responses_exports = {};
|
|
|
22
22
|
__export(responses_exports, {
|
|
23
23
|
Response: () => Response,
|
|
24
24
|
isResponse: () => isResponse,
|
|
25
|
-
responseBrand: () => responseBrand
|
|
25
|
+
responseBrand: () => responseBrand,
|
|
26
|
+
validateHttpStatusCode: () => validateHttpStatusCode
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(responses_exports);
|
|
28
29
|
var responseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.response");
|
|
30
|
+
var validateHttpStatusCode = (statusCode) => {
|
|
31
|
+
if (typeof statusCode !== "number" || !Number.isFinite(statusCode) || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) {
|
|
32
|
+
throw new TypeError("statusCode must be an integer HTTP status code between 100 and 599");
|
|
33
|
+
}
|
|
34
|
+
return statusCode;
|
|
35
|
+
};
|
|
29
36
|
var isResponse = (value) => {
|
|
30
37
|
if (value instanceof Response) {
|
|
31
38
|
return true;
|
|
@@ -41,7 +48,7 @@ _a = responseBrand;
|
|
|
41
48
|
var Response = class {
|
|
42
49
|
constructor(statusCode = 200, data) {
|
|
43
50
|
this[_a] = true;
|
|
44
|
-
this.statusCode = statusCode;
|
|
51
|
+
this.statusCode = validateHttpStatusCode(statusCode);
|
|
45
52
|
this.data = data;
|
|
46
53
|
}
|
|
47
54
|
};
|
|
@@ -49,5 +56,6 @@ var Response = class {
|
|
|
49
56
|
0 && (module.exports = {
|
|
50
57
|
Response,
|
|
51
58
|
isResponse,
|
|
52
|
-
responseBrand
|
|
59
|
+
responseBrand,
|
|
60
|
+
validateHttpStatusCode
|
|
53
61
|
});
|