express-fast-json-stringify 1.2.8 → 1.3.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.
@@ -0,0 +1,87 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ import { type Options } from 'fast-json-stringify';
3
+ import { type OverrideErrorHandler } from './override';
4
+ /**
5
+ * The parts of an OpenAPI 3.x or Swagger 2.0 document this package reads.
6
+ *
7
+ * It is deliberately a plain document rather than an integration with a
8
+ * specific library: every popular Express toolchain either consumes or produces
9
+ * one of these, so `swagger-jsdoc`, `swagger-ui-express`, `tsoa`,
10
+ * `express-openapi-validator` and a hand written file all work unchanged.
11
+ */
12
+ export type OpenApiDocument = {
13
+ readonly openapi?: string;
14
+ readonly swagger?: string;
15
+ readonly paths?: Readonly<Record<string, unknown>>;
16
+ readonly components?: Readonly<Record<string, unknown>>;
17
+ readonly definitions?: Readonly<Record<string, unknown>>;
18
+ };
19
+ export type OpenApiOptions = Omit<Options, 'mode'> & {
20
+ /** Media type to read the schema from. Defaults to `application/json`. */
21
+ readonly contentType?: string;
22
+ /** Pin the OpenAPI path instead of deriving it from the matched route. */
23
+ readonly path?: string;
24
+ /** Pin the OpenAPI method instead of using the request method. */
25
+ readonly method?: string;
26
+ /**
27
+ * Throw when the document describes no schema for a response, instead of
28
+ * quietly falling back to `res.json()`. Only applies to explicit
29
+ * `res.fastJson()` calls — an overridden `res.json()` always falls back.
30
+ */
31
+ readonly strict?: boolean;
32
+ /**
33
+ * Also route `res.json()` — and therefore `res.send(object)`, which Express
34
+ * implements on top of it — through the compiled serializer whenever the
35
+ * document describes the response.
36
+ *
37
+ * Off by default. Turning it on lets an existing codebase benefit without
38
+ * rewriting a single call site: responses the document covers are serialized
39
+ * from the schema, everything else keeps the stock behavior.
40
+ */
41
+ readonly overrideJson?: boolean;
42
+ /**
43
+ * Called when an overridden `res.json()` could not use the fast path because
44
+ * the serializer threw. The response falls back to the stock `res.json()`
45
+ * either way; this is only so the mismatch is visible.
46
+ */
47
+ readonly onError?: OverrideErrorHandler;
48
+ };
49
+ /**
50
+ * Translate an Express route pattern into the OpenAPI equivalent:
51
+ * `/users/:id` becomes `/users/{id}`. Express parameter modifiers — a trailing
52
+ * `?` or an inline `(regex)` — are dropped, since OpenAPI has no notion of them.
53
+ */
54
+ export declare const toOpenApiPath: (path: string) => string;
55
+ /**
56
+ * Build a stringify middleware that takes its schemas from an OpenAPI or Swagger
57
+ * document, so the contract you already publish is the one used to serialize.
58
+ *
59
+ * The operation is resolved per request from the matched Express route, and the
60
+ * schema from the response status code, which means `res.status(201).fastJson()`
61
+ * serializes with the `201` schema. Routes the document does not describe fall
62
+ * back to `res.json()` unless `strict` is set.
63
+ *
64
+ * @param {OpenApiDocument} document The OpenAPI 3.x or Swagger 2.0 document
65
+ * @param {OpenApiOptions} options The options to use (optional)
66
+ *
67
+ * Examples:
68
+ * ```ts
69
+ * import express from 'express';
70
+ * import swaggerJsdoc from 'swagger-jsdoc';
71
+ * import { fastJsonOpenApi } from 'express-fast-json-stringify';
72
+ *
73
+ * const app = express();
74
+ * const document = swaggerJsdoc({ definition: { openapi: '3.1.0', info: { title: 'API', version: '1.0.0' } }, apis: ['./routes/*.ts'] });
75
+ *
76
+ * app.use(fastJsonOpenApi(document));
77
+ *
78
+ * app.get('/users/:id', (req, res, next) => {
79
+ * try {
80
+ * res.fastJson({ id: Number(req.params.id), firstName: 'Simone' });
81
+ * } catch (error) {
82
+ * next(error);
83
+ * }
84
+ * });
85
+ * ```
86
+ */
87
+ export declare const fastJsonOpenApi: (document: OpenApiDocument, options?: OpenApiOptions) => (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.fastJsonOpenApi = exports.toOpenApiPath = void 0;
18
+ const fast_json_stringify_1 = __importDefault(require("fast-json-stringify"));
19
+ const override_1 = require("./override");
20
+ const send_1 = require("./send");
21
+ /**
22
+ * Translate an Express route pattern into the OpenAPI equivalent:
23
+ * `/users/:id` becomes `/users/{id}`. Express parameter modifiers — a trailing
24
+ * `?` or an inline `(regex)` — are dropped, since OpenAPI has no notion of them.
25
+ */
26
+ const toOpenApiPath = (path) => path.replace(/:([A-Za-z0-9_]+)(\([^)]*\))?\??/g, '{$1}');
27
+ exports.toOpenApiPath = toOpenApiPath;
28
+ /**
29
+ * Express answers a HEAD request with the GET handler, and documents rarely
30
+ * describe a `head` operation, so fall back to `get`.
31
+ */
32
+ const methodCandidates = (method) => (method === 'head' ? ['head', 'get'] : [method]);
33
+ /**
34
+ * Response keys to try, most specific first. OpenAPI allows a wildcard range
35
+ * (`2XX`) and a catch all (`default`) next to explicit codes.
36
+ */
37
+ const statusCandidates = (status) => {
38
+ const range = Math.floor(status / 100);
39
+ return [String(status), `${range}XX`, `${range}xx`, 'default'];
40
+ };
41
+ const findResponseSchema = (document, path, method, status, contentType) => {
42
+ var _a, _b, _c, _d;
43
+ const operations = (_a = document.paths) === null || _a === void 0 ? void 0 : _a[path];
44
+ if (!operations) {
45
+ return undefined;
46
+ }
47
+ for (const candidate of methodCandidates(method)) {
48
+ const operation = operations[candidate];
49
+ const responses = operation === null || operation === void 0 ? void 0 : operation.responses;
50
+ if (!responses) {
51
+ continue;
52
+ }
53
+ for (const key of statusCandidates(status)) {
54
+ const response = responses[key];
55
+ if (!response) {
56
+ continue;
57
+ }
58
+ // OpenAPI 3.x keys the schema by media type; Swagger 2.0 does not.
59
+ const schema = (_d = (_c = (_b = response.content) === null || _b === void 0 ? void 0 : _b[contentType]) === null || _c === void 0 ? void 0 : _c.schema) !== null && _d !== void 0 ? _d : response.schema;
60
+ if (schema) {
61
+ return schema;
62
+ }
63
+ }
64
+ }
65
+ return undefined;
66
+ };
67
+ /**
68
+ * fast-json-stringify resolves `$ref` as a JSON pointer against the root of the
69
+ * schema it is given, so the document's shared schemas only have to be reachable
70
+ * under the key the references already use — `components` for OpenAPI 3.x,
71
+ * `definitions` for Swagger 2.0. No rewriting needed, and recursive references
72
+ * keep working.
73
+ */
74
+ const withSharedSchemas = (document, schema) => {
75
+ const result = Object.assign({}, schema);
76
+ if (document.components && result.components === undefined) {
77
+ result.components = document.components;
78
+ }
79
+ if (document.definitions && result.definitions === undefined) {
80
+ result.definitions = document.definitions;
81
+ }
82
+ return result;
83
+ };
84
+ /**
85
+ * Build a stringify middleware that takes its schemas from an OpenAPI or Swagger
86
+ * document, so the contract you already publish is the one used to serialize.
87
+ *
88
+ * The operation is resolved per request from the matched Express route, and the
89
+ * schema from the response status code, which means `res.status(201).fastJson()`
90
+ * serializes with the `201` schema. Routes the document does not describe fall
91
+ * back to `res.json()` unless `strict` is set.
92
+ *
93
+ * @param {OpenApiDocument} document The OpenAPI 3.x or Swagger 2.0 document
94
+ * @param {OpenApiOptions} options The options to use (optional)
95
+ *
96
+ * Examples:
97
+ * ```ts
98
+ * import express from 'express';
99
+ * import swaggerJsdoc from 'swagger-jsdoc';
100
+ * import { fastJsonOpenApi } from 'express-fast-json-stringify';
101
+ *
102
+ * const app = express();
103
+ * const document = swaggerJsdoc({ definition: { openapi: '3.1.0', info: { title: 'API', version: '1.0.0' } }, apis: ['./routes/*.ts'] });
104
+ *
105
+ * app.use(fastJsonOpenApi(document));
106
+ *
107
+ * app.get('/users/:id', (req, res, next) => {
108
+ * try {
109
+ * res.fastJson({ id: Number(req.params.id), firstName: 'Simone' });
110
+ * } catch (error) {
111
+ * next(error);
112
+ * }
113
+ * });
114
+ * ```
115
+ */
116
+ const fastJsonOpenApi = (document, options = {}) => {
117
+ if (!document || typeof document !== 'object' || typeof document.paths !== 'object' || document.paths === null) {
118
+ throw new TypeError(`express-fast-json-stringify: invalid OpenAPI document`);
119
+ }
120
+ const { contentType = 'application/json', path: pinnedPath, method: pinnedMethod, strict = false, overrideJson = false, onError } = options, fastJsonOptions = __rest(options, ["contentType", "path", "method", "strict", "overrideJson", "onError"]);
121
+ // One compiled serializer per operation and status code. Misses are cached as
122
+ // `null` so an undocumented route costs a single lookup.
123
+ const serializers = new Map();
124
+ const routePath = (req) => { var _a, _b; return pinnedPath !== null && pinnedPath !== void 0 ? pinnedPath : (0, exports.toOpenApiPath)(`${req.baseUrl}${(_b = (_a = req.route) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : req.path}`); };
125
+ const serializerFor = (req, status) => {
126
+ const path = routePath(req);
127
+ const method = (pinnedMethod !== null && pinnedMethod !== void 0 ? pinnedMethod : req.method).toLowerCase();
128
+ const key = `${method} ${path} ${status}`;
129
+ const cached = serializers.get(key);
130
+ if (cached !== undefined) {
131
+ return cached;
132
+ }
133
+ const schema = findResponseSchema(document, path, method, status, contentType);
134
+ const serializer = schema ? (0, fast_json_stringify_1.default)(withSharedSchemas(document, schema), fastJsonOptions) : null;
135
+ serializers.set(key, serializer);
136
+ return serializer;
137
+ };
138
+ return (req, res, next) => {
139
+ /**
140
+ * Send JSON response, serialized with the schema the document declares for
141
+ * this route and status code.
142
+ *
143
+ * Examples:
144
+ * ```ts
145
+ * res.fastJson({ user: 'Simone Nigro' });
146
+ * res.status(201).fastJson({ user: 'Simone Nigro' });
147
+ * ```
148
+ */
149
+ res.fastJson = (body) => {
150
+ const serializer = serializerFor(req, res.statusCode);
151
+ if (!serializer) {
152
+ if (strict) {
153
+ throw new Error(`express-fast-json-stringify: no ${contentType} schema for ${req.method} ${routePath(req)} with status ${res.statusCode}`);
154
+ }
155
+ return res.json(body);
156
+ }
157
+ return (0, send_1.sendJson)(req, res, serializer(body));
158
+ };
159
+ if (overrideJson) {
160
+ (0, override_1.overrideResJson)(req, res, (status) => serializerFor(req, status), onError);
161
+ }
162
+ next();
163
+ };
164
+ };
165
+ exports.fastJsonOpenApi = fastJsonOpenApi;
166
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvb3BlbmFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7OztBQUNBLDhFQUEwRTtBQUUxRSx5Q0FBd0U7QUFDeEUsaUNBQWtDO0FBaURsQzs7OztHQUlHO0FBQ0ksTUFBTSxhQUFhLEdBQUcsQ0FBQyxJQUFZLEVBQVUsRUFBRSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsa0NBQWtDLEVBQUUsTUFBTSxDQUFDLENBQUM7QUFBbkcsUUFBQSxhQUFhLGlCQUFzRjtBQUVoSDs7O0dBR0c7QUFDSCxNQUFNLGdCQUFnQixHQUFHLENBQUMsTUFBYyxFQUFxQixFQUFFLENBQUMsQ0FBQyxNQUFNLEtBQUssTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDO0FBRWpIOzs7R0FHRztBQUNILE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxNQUFjLEVBQXFCLEVBQUU7SUFDN0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLEdBQUcsR0FBRyxDQUFDLENBQUM7SUFDdkMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxHQUFHLEtBQUssSUFBSSxFQUFFLEdBQUcsS0FBSyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7QUFDakUsQ0FBQyxDQUFDO0FBUUYsTUFBTSxrQkFBa0IsR0FBRyxDQUFDLFFBQXlCLEVBQUUsSUFBWSxFQUFFLE1BQWMsRUFBRSxNQUFjLEVBQUUsV0FBbUIsRUFBVyxFQUFFOztJQUNuSSxNQUFNLFVBQVUsR0FBRyxNQUFBLFFBQVEsQ0FBQyxLQUFLLDBDQUFHLElBQUksQ0FBa0QsQ0FBQztJQUMzRixJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7UUFDaEIsT0FBTyxTQUFTLENBQUM7SUFDbkIs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
@@ -0,0 +1,17 @@
1
+ import type { Request, Response } from 'express';
2
+ /** Resolves the serializer to use for a response status, or `null` for none. */
3
+ export type SerializerResolver = (status: number) => ((body: any) => string) | null;
4
+ /** Notified when an overridden `res.json()` could not use the fast path. */
5
+ export type OverrideErrorHandler = (error: unknown, req: Request) => void;
6
+ /**
7
+ * Route `res.json()` through a compiled serializer when one is available.
8
+ *
9
+ * Only `res.json` is replaced: Express implements `res.send(object)` by calling
10
+ * `res.json(object)`, so both entry points are covered by the single hook.
11
+ *
12
+ * The override is deliberately incapable of breaking a route. Whenever the fast
13
+ * path does not apply — no schema for this response, custom JSON settings, or a
14
+ * serializer that throws on the given body — the original `res.json()` runs and
15
+ * the response is exactly what it was before.
16
+ */
17
+ export declare const overrideResJson: (req: Request, res: Response, resolve: SerializerResolver, onError?: OverrideErrorHandler) => void;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.overrideResJson = void 0;
4
+ const send_1 = require("./send");
5
+ /**
6
+ * Settings that change the bytes `res.json()` writes. A compiled serializer
7
+ * cannot reproduce any of them, so the fast path has to step aside when one is
8
+ * configured rather than silently change the output.
9
+ */
10
+ const hasCustomJsonSettings = (res) => {
11
+ var _a, _b;
12
+ const app = res.app;
13
+ if (!app) {
14
+ return false;
15
+ }
16
+ return Boolean((_b = (_a = app.get('json replacer')) !== null && _a !== void 0 ? _a : app.get('json spaces')) !== null && _b !== void 0 ? _b : app.get('json escape'));
17
+ };
18
+ /**
19
+ * Route `res.json()` through a compiled serializer when one is available.
20
+ *
21
+ * Only `res.json` is replaced: Express implements `res.send(object)` by calling
22
+ * `res.json(object)`, so both entry points are covered by the single hook.
23
+ *
24
+ * The override is deliberately incapable of breaking a route. Whenever the fast
25
+ * path does not apply — no schema for this response, custom JSON settings, or a
26
+ * serializer that throws on the given body — the original `res.json()` runs and
27
+ * the response is exactly what it was before.
28
+ */
29
+ const overrideResJson = (req, res, resolve, onError) => {
30
+ const original = res.json.bind(res);
31
+ res.json = (body) => {
32
+ if (!hasCustomJsonSettings(res)) {
33
+ const serialize = resolve(res.statusCode);
34
+ if (serialize) {
35
+ try {
36
+ return (0, send_1.sendJson)(req, res, serialize(body));
37
+ }
38
+ catch (error) {
39
+ // A body that does not fit the schema must not turn into a failed
40
+ // request: report it, then fall through to the stock res.json().
41
+ onError === null || onError === void 0 ? void 0 : onError(error, req);
42
+ }
43
+ }
44
+ }
45
+ return original(body);
46
+ };
47
+ };
48
+ exports.overrideResJson = overrideResJson;
49
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3ZlcnJpZGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbGliL292ZXJyaWRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUVBLGlDQUFrQztBQVFsQzs7OztHQUlHO0FBQ0gsTUFBTSxxQkFBcUIsR0FBRyxDQUFDLEdBQWEsRUFBVyxFQUFFOztJQUN2RCxNQUFNLEdBQUcsR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDO0lBQ3BCLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUNULE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE9BQU8sT0FBTyxDQUFDLE1BQUEsTUFBQSxHQUFHLENBQUMsR0FBRyxDQUFDLGVBQWUsQ0FBQyxtQ0FBSSxHQUFHLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxtQ0FBSSxHQUFHLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUM7QUFDL0YsQ0FBQyxDQUFDO0FBRUY7Ozs7Ozs7Ozs7R0FVRztBQUNJLE1BQU0sZUFBZSxHQUFHLENBQUMsR0FBWSxFQUFFLEdBQWEsRUFBRSxPQUEyQixFQUFFLE9BQThCLEVBQVEsRUFBRTtJQUNoSSxNQUFNLFFBQVEsR0FBRyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUVwQyxHQUFHLENBQUMsSUFBSSxHQUFHLENBQUMsSUFBUyxFQUFZLEVBQUU7UUFDakMsSUFBSSxDQUFDLHFCQUFxQixDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7WUFDaEMsTUFBTSxTQUFTLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsQ0FBQztZQUMxQyxJQUFJLFNBQVMsRUFBRSxDQUFDO2dCQUNkLElBQUksQ0FBQztvQkFDSCxPQUFPLElBQUEsZUFBUSxFQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7Z0JBQzdDLENBQUM7Z0JBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztvQkFDZixrRUFBa0U7b0JBQ2xFLGlFQUFpRTtvQkFDakUsT0FBTyxhQUFQLE9BQU8sdUJBQVAsT0FBTyxDQUFHLEtBQUssRUFBRSxHQUFHLENBQUMsQ0FBQztnQkFDeEIsQ0FBQztZQUNILENBQUM7UUFDSCxDQUFDO1FBQ0QsT0FBTyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEIsQ0FBQyxDQUFDO0FBQ0osQ0FBQyxDQUFDO0FBbEJXLFFBQUEsZUFBZSxtQkFrQjFCIn0=
@@ -0,0 +1,10 @@
1
+ import type { Request, Response } from 'express';
2
+ /**
3
+ * Write an already serialized JSON payload with the same HTTP semantics as
4
+ * `res.json()`: charset, `Content-Length`, `ETag`, conditional requests and the
5
+ * empty body rules for `204`/`304`.
6
+ *
7
+ * Every middleware in this package goes through here, so they cannot drift
8
+ * apart from each other or from Express.
9
+ */
10
+ export declare const sendJson: (req: Request, res: Response, serialized: string) => Response;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendJson = void 0;
4
+ /**
5
+ * Write an already serialized JSON payload with the same HTTP semantics as
6
+ * `res.json()`: charset, `Content-Length`, `ETag`, conditional requests and the
7
+ * empty body rules for `204`/`304`.
8
+ *
9
+ * Every middleware in this package goes through here, so they cannot drift
10
+ * apart from each other or from Express.
11
+ */
12
+ const sendJson = (req, res, serialized) => {
13
+ var _a;
14
+ let payload = serialized;
15
+ // Do not clobber a content type the route set on purpose (eg. res.type('application/vnd.api+json')).
16
+ if (!res.getHeader('Content-Type')) {
17
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
18
+ }
19
+ // Mirror res.send(): honour the app `etag` setting so that swapping
20
+ // res.json() for res.fastJson() keeps conditional requests working.
21
+ const etagFn = (_a = res.app) === null || _a === void 0 ? void 0 : _a.get('etag fn');
22
+ if (typeof etagFn === 'function' && !res.getHeader('ETag')) {
23
+ const etag = etagFn(payload, 'utf-8');
24
+ if (etag) {
25
+ res.setHeader('ETag', etag);
26
+ }
27
+ }
28
+ if (req.fresh) {
29
+ res.statusCode = 304;
30
+ }
31
+ // 204 No Content and 304 Not Modified must not carry a body, nor describe one.
32
+ if (res.statusCode === 204 || res.statusCode === 304) {
33
+ res.removeHeader('Content-Type');
34
+ res.removeHeader('Content-Length');
35
+ res.removeHeader('Transfer-Encoding');
36
+ payload = '';
37
+ }
38
+ else {
39
+ // Without this the response falls back to chunked encoding, and HEAD
40
+ // requests answer with no length at all.
41
+ res.setHeader('Content-Length', Buffer.byteLength(payload));
42
+ }
43
+ return res.end(payload);
44
+ };
45
+ exports.sendJson = sendJson;
46
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VuZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvc2VuZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFFQTs7Ozs7OztHQU9HO0FBQ0ksTUFBTSxRQUFRLEdBQUcsQ0FBQyxHQUFZLEVBQUUsR0FBYSxFQUFFLFVBQWtCLEVBQVksRUFBRTs7SUFDcEYsSUFBSSxPQUFPLEdBQUcsVUFBVSxDQUFDO0lBRXpCLHFHQUFxRztJQUNyRyxJQUFJLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxjQUFjLENBQUMsRUFBRSxDQUFDO1FBQ25DLEdBQUcsQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFLGlDQUFpQyxDQUFDLENBQUM7SUFDbkUsQ0FBQztJQUVELG9FQUFvRTtJQUNwRSxvRUFBb0U7SUFDcEUsTUFBTSxNQUFNLEdBQUcsTUFBQSxHQUFHLENBQUMsR0FBRywwQ0FBRSxHQUFHLENBQUMsU0FBUyxDQUE0RSxDQUFDO0lBQ2xILElBQUksT0FBTyxNQUFNLEtBQUssVUFBVSxJQUFJLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQzNELE1BQU0sSUFBSSxHQUFHLE1BQU0sQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDdEMsSUFBSSxJQUFJLEVBQUUsQ0FBQztZQUNULEdBQUcsQ0FBQyxTQUFTLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQzlCLENBQUM7SUFDSCxDQUFDO0lBRUQsSUFBSSxHQUFHLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDZCxHQUFHLENBQUMsVUFBVSxHQUFHLEdBQUcsQ0FBQztJQUN2QixDQUFDO0lBRUQsK0VBQStFO0lBQy9FLElBQUksR0FBRyxDQUFDLFVBQVUsS0FBSyxHQUFHLElBQUksR0FBRyxDQUFDLFVBQVUsS0FBSyxHQUFHLEVBQUUsQ0FBQztRQUNyRCxHQUFHLENBQUMsWUFBWSxDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ2pDLEdBQUcsQ0FBQyxZQUFZLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztRQUNuQyxHQUFHLENBQUMsWUFBWSxDQUFDLG1CQUFtQixDQUFDLENBQUM7UUFDdEMsT0FBTyxHQUFHLEVBQUUsQ0FBQztJQUNmLENBQUM7U0FBTSxDQUFDO1FBQ04scUVBQXFFO1FBQ3JFLHlDQUF5QztRQUN6QyxHQUFHLENBQUMsU0FBUyxDQUFDLGdCQUFnQixFQUFFLE1BQU0sQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztJQUM5RCxDQUFDO0lBRUQsT0FBTyxHQUFHLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0FBQzFCLENBQUMsQ0FBQztBQW5DVyxRQUFBLFFBQVEsWUFtQ25CIn0=
@@ -1 +1,3 @@
1
1
  export * from './lib/middleware';
2
+ export * from './lib/openapi';
3
+ export type { OverrideErrorHandler } from './lib/override';
@@ -1,2 +1,3 @@
1
1
  export * from './lib/middleware';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxrQkFBa0IsQ0FBQyJ9
2
+ export * from './lib/openapi';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLGVBQWUsQ0FBQyJ9
@@ -1,8 +1,26 @@
1
1
  import type { NextFunction, Request, Response } from 'express';
2
2
  import { type Options, type Schema } from 'fast-json-stringify';
3
+ import { type OverrideErrorHandler } from './override';
3
4
  export type { Schema, Options } from 'fast-json-stringify';
5
+ export type FastJsonSchemaOptions = Omit<Options, 'mode'> & {
6
+ /**
7
+ * Also route `res.json()` — and therefore `res.send(object)`, which Express
8
+ * implements on top of it — through the compiled serializer.
9
+ *
10
+ * Off by default. Because a single schema describes the successful payload,
11
+ * only `2xx` responses take the fast path: an error body would otherwise be
12
+ * rewritten into the shape of the success schema.
13
+ */
14
+ readonly overrideJson?: boolean;
15
+ /**
16
+ * Called when an overridden `res.json()` could not use the fast path because
17
+ * the serializer threw. The response falls back to the stock `res.json()`
18
+ * either way; this is only so the mismatch is visible.
19
+ */
20
+ readonly onError?: OverrideErrorHandler;
21
+ };
4
22
  /**
5
- * Set the schema
23
+ * Build a stringify function using a schema of the documents that should be stringified
6
24
  * @param {Schema} schema The schema used to stringify values
7
25
  * @param {Options} options The options to use (optional)
8
26
  * @see https://www.npmjs.com/package/fast-json-stringify
@@ -10,7 +28,7 @@ export type { Schema, Options } from 'fast-json-stringify';
10
28
  * Examples:
11
29
  * ```ts
12
30
  * import express from 'express';
13
- * import { fastJsonSchemas, Schema } from 'express-fast-json-stringify';
31
+ * import { fastJsonSchema, Schema } from 'express-fast-json-stringify';
14
32
  *
15
33
  * const app = express();
16
34
  *
@@ -44,7 +62,7 @@ export type { Schema, Options } from 'fast-json-stringify';
44
62
  * });
45
63
  * ```
46
64
  */
47
- export declare const fastJsonSchema: (schema: Schema, options?: Options) => (_req: Request, res: Response, next: NextFunction) => void;
65
+ export declare const fastJsonSchema: (schema: Schema, options?: FastJsonSchemaOptions) => (req: Request, res: Response, next: NextFunction) => void;
48
66
  declare global {
49
67
  namespace Express {
50
68
  interface Response {
@@ -53,11 +71,11 @@ declare global {
53
71
  *
54
72
  * Examples:
55
73
  * ```ts
56
- * res.fastJson({ user: 'tj' });
57
- * res.status(200).fastJson({ user: 'tj' });
74
+ * res.fastJson({ user: 'Simone Nigro' });
75
+ * res.status(200).fastJson({ user: 'Simone Nigro' });
58
76
  * ```
59
77
  */
60
- fastJson: (data: any) => Response;
78
+ fastJson: (body: any) => Response;
61
79
  }
62
80
  }
63
81
  }
@@ -1,6 +1,8 @@
1
1
  import fastJson from 'fast-json-stringify';
2
+ import { overrideResJson } from './override';
3
+ import { sendJson } from './send';
2
4
  /**
3
- * Set the schema
5
+ * Build a stringify function using a schema of the documents that should be stringified
4
6
  * @param {Schema} schema The schema used to stringify values
5
7
  * @param {Options} options The options to use (optional)
6
8
  * @see https://www.npmjs.com/package/fast-json-stringify
@@ -8,7 +10,7 @@ import fastJson from 'fast-json-stringify';
8
10
  * Examples:
9
11
  * ```ts
10
12
  * import express from 'express';
11
- * import { fastJsonSchemas, Schema } from 'express-fast-json-stringify';
13
+ * import { fastJsonSchema, Schema } from 'express-fast-json-stringify';
12
14
  *
13
15
  * const app = express();
14
16
  *
@@ -43,27 +45,29 @@ import fastJson from 'fast-json-stringify';
43
45
  * ```
44
46
  */
45
47
  export const fastJsonSchema = (schema, options) => {
46
- if (!schema) {
48
+ if (!schema || (typeof schema !== 'object' && typeof schema !== 'boolean')) {
47
49
  throw new TypeError(`express-fast-json-stringify: invalid schema`);
48
50
  }
49
- const fjs = fastJson(schema, options);
50
- return (_req, res, next) => {
51
+ const { overrideJson = false, onError, ...fastJsonOptions } = options ?? {};
52
+ const fjs = fastJson(schema, fastJsonOptions);
53
+ return (req, res, next) => {
51
54
  /**
52
55
  * Send JSON response.
53
56
  *
54
57
  * Examples:
55
58
  * ```ts
56
- * res.fastJson({ user: 'tj' });
57
- * res.status(200).fastJson({ user: 'tj' });
59
+ * res.fastJson({ user: 'Simone Nigro' });
60
+ * res.status(200).fastJson({ user: 'Simone Nigro' });
58
61
  * ```
59
62
  */
60
- res.fastJson = (data) => {
61
- if (!res.getHeader('Content-Type')) {
62
- res.setHeader('Content-Type', 'application/json');
63
- }
64
- return res.send(fjs(data));
65
- };
63
+ res.fastJson = (body) => sendJson(req, res, fjs(body));
64
+ if (overrideJson) {
65
+ // A single schema describes the successful payload, so applying it to an
66
+ // error body would rewrite it into the wrong shape. Only 2xx responses
67
+ // take the fast path; everything else keeps the stock res.json().
68
+ overrideResJson(req, res, (status) => (status >= 200 && status < 300 ? fjs : null), onError);
69
+ }
66
70
  next();
67
71
  };
68
72
  };
69
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvbWlkZGxld2FyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLFFBQXVDLE1BQU0scUJBQXFCLENBQUM7QUFJMUU7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQTBDRztBQUNILE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxDQUFDLE1BQWMsRUFBRSxPQUFpQixFQUFFLEVBQUU7SUFDbEUsSUFBSSxDQUFDLE1BQU0sRUFBRTtRQUNYLE1BQU0sSUFBSSxTQUFTLENBQUMsNkNBQTZDLENBQUMsQ0FBQztLQUNwRTtJQUNELE1BQU0sR0FBRyxHQUFHLFFBQVEsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDdEMsT0FBTyxDQUFDLElBQWEsRUFBRSxHQUFhLEVBQUUsSUFBa0IsRUFBRSxFQUFFO1FBQzFEOzs7Ozs7OztXQVFHO1FBQ0gsR0FBRyxDQUFDLFFBQVEsR0FBRyxDQUFDLElBQVMsRUFBWSxFQUFFO1lBQ3JDLElBQUksQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLGNBQWMsQ0FBQyxFQUFFO2dCQUNsQyxHQUFHLENBQUMsU0FBUyxDQUFDLGNBQWMsRUFBRSxrQkFBa0IsQ0FBQyxDQUFDO2FBQ25EO1lBQ0QsT0FBTyxHQUFHLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQzdCLENBQUMsQ0FBQztRQUNGLElBQUksRUFBRSxDQUFDO0lBQ1QsQ0FBQyxDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
73
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvbWlkZGxld2FyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLFFBQXVDLE1BQU0scUJBQXFCLENBQUM7QUFFMUUsT0FBTyxFQUE2QixlQUFlLEVBQUUsTUFBTSxZQUFZLENBQUM7QUFDeEUsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLFFBQVEsQ0FBQztBQXNCbEM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQTBDRztBQUNILE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxDQUFDLE1BQWMsRUFBRSxPQUErQixFQUFFLEVBQUU7SUFDaEYsSUFBSSxDQUFDLE1BQU0sSUFBSSxDQUFDLE9BQU8sTUFBTSxLQUFLLFFBQVEsSUFBSSxPQUFPLE1BQU0sS0FBSyxTQUFTLENBQUMsRUFBRSxDQUFDO1FBQzNFLE1BQU0sSUFBSSxTQUFTLENBQUMsNkNBQTZDLENBQUMsQ0FBQztJQUNyRSxDQUFDO0lBQ0QsTUFBTSxFQUFFLFlBQVksR0FBRyxLQUFLLEVBQUUsT0FBTyxFQUFFLEdBQUcsZUFBZSxFQUFFLEdBQUcsT0FBTyxJQUFJLEVBQUUsQ0FBQztJQUM1RSxNQUFNLEdBQUcsR0FBRyxRQUFRLENBQUMsTUFBTSxFQUFFLGVBQWUsQ0FBQyxDQUFDO0lBQzlDLE9BQU8sQ0FBQyxHQUFZLEVBQUUsR0FBYSxFQUFFLElBQWtCLEVBQUUsRUFBRTtRQUN6RDs7Ozs7Ozs7V0FRRztRQUNILEdBQUcsQ0FBQyxRQUFRLEdBQUcsQ0FBQyxJQUFTLEVBQVksRUFBRSxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBRXRFLElBQUksWUFBWSxFQUFFLENBQUM7WUFDakIseUVBQXlFO1lBQ3pFLHVFQUF1RTtZQUN2RSxrRUFBa0U7WUFDbEUsZUFBZSxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUMsTUFBTSxJQUFJLEdBQUcsSUFBSSxNQUFNLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBQy9GLENBQUM7UUFFRCxJQUFJLEVBQUUsQ0FBQztJQUNULENBQUMsQ0FBQztBQUNKLENBQUMsQ0FBQyJ9
@@ -0,0 +1,87 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ import { type Options } from 'fast-json-stringify';
3
+ import { type OverrideErrorHandler } from './override';
4
+ /**
5
+ * The parts of an OpenAPI 3.x or Swagger 2.0 document this package reads.
6
+ *
7
+ * It is deliberately a plain document rather than an integration with a
8
+ * specific library: every popular Express toolchain either consumes or produces
9
+ * one of these, so `swagger-jsdoc`, `swagger-ui-express`, `tsoa`,
10
+ * `express-openapi-validator` and a hand written file all work unchanged.
11
+ */
12
+ export type OpenApiDocument = {
13
+ readonly openapi?: string;
14
+ readonly swagger?: string;
15
+ readonly paths?: Readonly<Record<string, unknown>>;
16
+ readonly components?: Readonly<Record<string, unknown>>;
17
+ readonly definitions?: Readonly<Record<string, unknown>>;
18
+ };
19
+ export type OpenApiOptions = Omit<Options, 'mode'> & {
20
+ /** Media type to read the schema from. Defaults to `application/json`. */
21
+ readonly contentType?: string;
22
+ /** Pin the OpenAPI path instead of deriving it from the matched route. */
23
+ readonly path?: string;
24
+ /** Pin the OpenAPI method instead of using the request method. */
25
+ readonly method?: string;
26
+ /**
27
+ * Throw when the document describes no schema for a response, instead of
28
+ * quietly falling back to `res.json()`. Only applies to explicit
29
+ * `res.fastJson()` calls — an overridden `res.json()` always falls back.
30
+ */
31
+ readonly strict?: boolean;
32
+ /**
33
+ * Also route `res.json()` — and therefore `res.send(object)`, which Express
34
+ * implements on top of it — through the compiled serializer whenever the
35
+ * document describes the response.
36
+ *
37
+ * Off by default. Turning it on lets an existing codebase benefit without
38
+ * rewriting a single call site: responses the document covers are serialized
39
+ * from the schema, everything else keeps the stock behavior.
40
+ */
41
+ readonly overrideJson?: boolean;
42
+ /**
43
+ * Called when an overridden `res.json()` could not use the fast path because
44
+ * the serializer threw. The response falls back to the stock `res.json()`
45
+ * either way; this is only so the mismatch is visible.
46
+ */
47
+ readonly onError?: OverrideErrorHandler;
48
+ };
49
+ /**
50
+ * Translate an Express route pattern into the OpenAPI equivalent:
51
+ * `/users/:id` becomes `/users/{id}`. Express parameter modifiers — a trailing
52
+ * `?` or an inline `(regex)` — are dropped, since OpenAPI has no notion of them.
53
+ */
54
+ export declare const toOpenApiPath: (path: string) => string;
55
+ /**
56
+ * Build a stringify middleware that takes its schemas from an OpenAPI or Swagger
57
+ * document, so the contract you already publish is the one used to serialize.
58
+ *
59
+ * The operation is resolved per request from the matched Express route, and the
60
+ * schema from the response status code, which means `res.status(201).fastJson()`
61
+ * serializes with the `201` schema. Routes the document does not describe fall
62
+ * back to `res.json()` unless `strict` is set.
63
+ *
64
+ * @param {OpenApiDocument} document The OpenAPI 3.x or Swagger 2.0 document
65
+ * @param {OpenApiOptions} options The options to use (optional)
66
+ *
67
+ * Examples:
68
+ * ```ts
69
+ * import express from 'express';
70
+ * import swaggerJsdoc from 'swagger-jsdoc';
71
+ * import { fastJsonOpenApi } from 'express-fast-json-stringify';
72
+ *
73
+ * const app = express();
74
+ * const document = swaggerJsdoc({ definition: { openapi: '3.1.0', info: { title: 'API', version: '1.0.0' } }, apis: ['./routes/*.ts'] });
75
+ *
76
+ * app.use(fastJsonOpenApi(document));
77
+ *
78
+ * app.get('/users/:id', (req, res, next) => {
79
+ * try {
80
+ * res.fastJson({ id: Number(req.params.id), firstName: 'Simone' });
81
+ * } catch (error) {
82
+ * next(error);
83
+ * }
84
+ * });
85
+ * ```
86
+ */
87
+ export declare const fastJsonOpenApi: (document: OpenApiDocument, options?: OpenApiOptions) => (req: Request, res: Response, next: NextFunction) => void;