Skip to content

Commit 028468e

Browse files
committed
feat: Export as ESM.
1 parent cdeaa9b commit 028468e

15 files changed

+410
-13
lines changed
File renamed without changes.

lib/index.js renamed to dist/cjs/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,9 @@ exports.plugin = fastify_plugin_1.default(function (instance, options, done) {
4949
done();
5050
}, { name: 'fastify-http-errors-enhanced' });
5151
exports.default = exports.plugin;
52-
module.exports = exports.plugin;
53-
Object.assign(module.exports, exports);
52+
// Fix CommonJS exporting
53+
/* istanbul ignore else */
54+
if (typeof module !== 'undefined') {
55+
module.exports = exports.plugin;
56+
Object.assign(module.exports, exports);
57+
}
File renamed without changes.
File renamed without changes.

lib/validation.js renamed to dist/cjs/validation.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,16 @@ function addResponseValidation(route) {
207207
}
208208
exports.addResponseValidation = addResponseValidation;
209209
function compileResponseValidationSchema() {
210+
// Fix CJS/ESM interoperability
211+
// @ts-expect-error
212+
let AjvConstructor = ajv_1.default;
213+
/* istanbul ignore next */
214+
if (AjvConstructor.default) {
215+
AjvConstructor = AjvConstructor.default;
216+
}
210217
for (const [instance, validators, schemas] of this[interfaces_1.kHttpErrorsEnhancedResponseValidations]) {
211-
const compiler = new ajv_1.default({
218+
// @ts-expect-error
219+
const compiler = new AjvConstructor({
212220
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
213221
removeAdditional: false,
214222
useDefaults: true,

dist/mjs/handlers.mjs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { addAdditionalProperties, BadRequestError, InternalServerError, INTERNAL_SERVER_ERROR, messagesByCodes, NotFoundError, serializeError, UnsupportedMediaTypeError } from 'http-errors-enhanced';
2+
import { kHttpErrorsEnhancedProperties } from "./interfaces.mjs";
3+
import { upperFirst } from "./utils.mjs";
4+
import { convertValidationErrors, validationMessagesFormatters } from "./validation.mjs";
5+
export function handleNotFoundError(request, reply) {
6+
handleErrors(new NotFoundError('Not found.'), request, reply);
7+
}
8+
export function handleValidationError(error, request) {
9+
/*
10+
As seen in https://github.com/fastify/fastify/blob/master/lib/validation.js
11+
the error.message will always start with the relative section (params, querystring, headers, body)
12+
and fastify throws on first failing section.
13+
*/
14+
const section = error.message.match(/^\w+/)[0];
15+
return new BadRequestError('One or more validations failed trying to process your request.', {
16+
failedValidations: convertValidationErrors(section, Reflect.get(request, section), error.validation)
17+
});
18+
}
19+
export function handleErrors(error, request, reply) {
20+
var _a, _b;
21+
// It is a generic error, handle it
22+
const code = error.code;
23+
if (!('statusCode' in error)) {
24+
if ('validation' in error && ((_a = request[kHttpErrorsEnhancedProperties]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
25+
// If it is a validation error, convert errors to human friendly format
26+
error = handleValidationError(error, request);
27+
}
28+
else if ((_b = request[kHttpErrorsEnhancedProperties]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
29+
// It is requested to hide the error, just log it and then create a generic one
30+
request.log.error({ error: serializeError(error) });
31+
error = new InternalServerError('An error occurred trying to process your request.');
32+
}
33+
else {
34+
// Wrap in a HttpError, making the stack explicitily available
35+
error = new InternalServerError(serializeError(error));
36+
Object.defineProperty(error, 'stack', { enumerable: true });
37+
}
38+
}
39+
else if (code === 'INVALID_CONTENT_TYPE' || code === 'FST_ERR_CTP_INVALID_MEDIA_TYPE') {
40+
error = new UnsupportedMediaTypeError(upperFirst(validationMessagesFormatters.contentType()));
41+
}
42+
else if (code === 'FST_ERR_CTP_EMPTY_JSON_BODY') {
43+
error = new BadRequestError(upperFirst(validationMessagesFormatters.jsonEmpty()));
44+
}
45+
else if (code === 'MALFORMED_JSON' || error.message === 'Invalid JSON' || error.stack.includes('at JSON.parse')) {
46+
error = new BadRequestError(upperFirst(validationMessagesFormatters.json()));
47+
}
48+
// Get the status code
49+
let { statusCode, headers } = error;
50+
// Code outside HTTP range
51+
if (statusCode < 100 || statusCode > 599) {
52+
statusCode = INTERNAL_SERVER_ERROR;
53+
}
54+
// Create the body
55+
const body = {
56+
statusCode,
57+
error: messagesByCodes[statusCode],
58+
message: error.message
59+
};
60+
addAdditionalProperties(body, error);
61+
// Send the error back
62+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
63+
reply
64+
.code(statusCode)
65+
.headers(headers !== null && headers !== void 0 ? headers : {})
66+
.type('application/json')
67+
.send(body);
68+
}

dist/mjs/index.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import fastifyPlugin from 'fastify-plugin';
2+
import { handleErrors, handleNotFoundError } from "./handlers.mjs";
3+
import { kHttpErrorsEnhancedProperties, kHttpErrorsEnhancedResponseValidations } from "./interfaces.mjs";
4+
import { addResponseValidation, compileResponseValidationSchema } from "./validation.mjs";
5+
export * from "./handlers.mjs";
6+
export * from "./interfaces.mjs";
7+
export { convertValidationErrors, niceJoin, validationMessagesFormatters } from "./validation.mjs";
8+
export const plugin = fastifyPlugin(function (instance, options, done) {
9+
var _a, _b, _c, _d;
10+
const isProduction = process.env.NODE_ENV === 'production';
11+
const hideUnhandledErrors = (_a = options.hideUnhandledErrors) !== null && _a !== void 0 ? _a : isProduction;
12+
const convertValidationErrors = (_b = options.convertValidationErrors) !== null && _b !== void 0 ? _b : true;
13+
const convertResponsesValidationErrors = (_c = options.convertResponsesValidationErrors) !== null && _c !== void 0 ? _c : !isProduction;
14+
const allowUndeclaredResponses = (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false;
15+
instance.decorateRequest(kHttpErrorsEnhancedProperties, null);
16+
instance.addHook('onRequest', async (request) => {
17+
request[kHttpErrorsEnhancedProperties] = {
18+
hideUnhandledErrors,
19+
convertValidationErrors,
20+
allowUndeclaredResponses
21+
};
22+
});
23+
instance.setErrorHandler(handleErrors);
24+
instance.setNotFoundHandler(handleNotFoundError);
25+
if (convertResponsesValidationErrors) {
26+
instance.decorate(kHttpErrorsEnhancedResponseValidations, []);
27+
instance.addHook('onRoute', addResponseValidation);
28+
instance.addHook('onReady', compileResponseValidationSchema);
29+
}
30+
done();
31+
}, { name: 'fastify-http-errors-enhanced' });
32+
export default plugin;
33+
// Fix CommonJS exporting
34+
/* istanbul ignore else */
35+
if (typeof module !== 'undefined') {
36+
module.exports = plugin;
37+
Object.assign(module.exports, exports);
38+
}

dist/mjs/interfaces.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const kHttpErrorsEnhancedProperties = Symbol('fastify-http-errors-enhanced-properties');
2+
export const kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation');

dist/mjs/utils.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export function upperFirst(source) {
2+
if (typeof source !== 'string' || !source.length) {
3+
return source;
4+
}
5+
return source[0].toUpperCase() + source.substring(1);
6+
}
7+
export function get(target, path) {
8+
var _a;
9+
const tokens = path.split('.').map((t) => t.trim());
10+
for (const token of tokens) {
11+
if (typeof target === 'undefined' || target === null) {
12+
// We're supposed to be still iterating, but the chain is over - Return undefined
13+
target = undefined;
14+
break;
15+
}
16+
const index = token.match(/^(\d+)|(?:\[(\d+)\])$/);
17+
if (index) {
18+
target = target[parseInt((_a = index[1]) !== null && _a !== void 0 ? _a : index[2], 10)];
19+
}
20+
else {
21+
target = target[token];
22+
}
23+
}
24+
return target;
25+
}

dist/mjs/validation.mjs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import Ajv from 'ajv';
2+
import { InternalServerError, INTERNAL_SERVER_ERROR } from 'http-errors-enhanced';
3+
import { kHttpErrorsEnhancedProperties, kHttpErrorsEnhancedResponseValidations } from "./interfaces.mjs";
4+
import { get } from "./utils.mjs";
5+
export function niceJoin(array, lastSeparator = ' and ', separator = ', ') {
6+
switch (array.length) {
7+
case 0:
8+
return '';
9+
case 1:
10+
return array[0];
11+
case 2:
12+
return array.join(lastSeparator);
13+
default:
14+
return array.slice(0, array.length - 1).join(separator) + lastSeparator + array[array.length - 1];
15+
}
16+
}
17+
export const validationMessagesFormatters = {
18+
contentType: () => 'only JSON payloads are accepted. Please set the "Content-Type" header to start with "application/json"',
19+
json: () => 'the body payload is not a valid JSON',
20+
jsonEmpty: () => 'the JSON body payload cannot be empty if the "Content-Type" header is set',
21+
missing: () => 'must be present',
22+
unknown: () => 'is not a valid property',
23+
uuid: () => 'must be a valid GUID (UUID v4)',
24+
timestamp: () => 'must be a valid ISO 8601 / RFC 3339 timestamp (example: 2018-07-06T12:34:56Z)',
25+
date: () => 'must be a valid ISO 8601 / RFC 3339 date (example: 2018-07-06)',
26+
time: () => 'must be a valid ISO 8601 / RFC 3339 time (example: 12:34:56)',
27+
hostname: () => 'must be a valid hostname',
28+
ipv4: () => 'must be a valid IPv4',
29+
ipv6: () => 'must be a valid IPv6',
30+
paramType: (type) => {
31+
switch (type) {
32+
case 'integer':
33+
return 'must be a valid integer number';
34+
case 'number':
35+
return 'must be a valid number';
36+
case 'boolean':
37+
return 'must be a valid boolean (true or false)';
38+
case 'object':
39+
return 'must be a object';
40+
case 'array':
41+
return 'must be an array';
42+
default:
43+
return 'must be a string';
44+
}
45+
},
46+
presentString: () => 'must be a non empty string',
47+
minimum: (min) => `must be a number greater than or equal to ${min}`,
48+
maximum: (max) => `must be a number less than or equal to ${max}`,
49+
minimumProperties(min) {
50+
return min === 1 ? 'cannot be a empty object' : `must be a object with at least ${min} properties`;
51+
},
52+
maximumProperties(max) {
53+
return max === 0 ? 'must be a empty object' : `must be a object with at most ${max} properties`;
54+
},
55+
minimumItems(min) {
56+
return min === 1 ? 'cannot be a empty array' : `must be an array with at least ${min} items`;
57+
},
58+
maximumItems(max) {
59+
return max === 0 ? 'must be a empty array' : `must be an array with at most ${max} items`;
60+
},
61+
enum: (values) => `must be one of the following values: ${niceJoin(values.map((f) => `"${f}"`), ' or ')}`,
62+
pattern: (pattern) => `must match pattern "${pattern.replace(/\(\?:/g, '(')}"`,
63+
invalidResponseCode: (code) => `This endpoint cannot respond with HTTP status ${code}.`,
64+
invalidResponse: (code) => `The response returned from the endpoint violates its specification for the HTTP status ${code}.`,
65+
invalidFormat: (format) => `must match format "${format}" (format)`
66+
};
67+
export function convertValidationErrors(section, data, validationErrors) {
68+
const errors = {};
69+
if (section === 'querystring') {
70+
section = 'query';
71+
}
72+
// For each error
73+
for (const e of validationErrors) {
74+
let message = '';
75+
let pattern;
76+
let value;
77+
let reason;
78+
// Normalize the key
79+
let key = e.dataPath;
80+
if (key.startsWith('.')) {
81+
key = key.substring(1);
82+
}
83+
// Remove useless quotes
84+
/* istanbul ignore next */
85+
if (key.startsWith('[') && key.endsWith(']')) {
86+
key = key.substring(1, key.length - 1);
87+
}
88+
// Depending on the type
89+
switch (e.keyword) {
90+
case 'required':
91+
case 'dependencies':
92+
key = e.params.missingProperty;
93+
message = validationMessagesFormatters.missing();
94+
break;
95+
case 'additionalProperties':
96+
key = e.params.additionalProperty;
97+
message = validationMessagesFormatters.unknown();
98+
break;
99+
case 'type':
100+
message = validationMessagesFormatters.paramType(e.params.type);
101+
break;
102+
case 'minProperties':
103+
message = validationMessagesFormatters.minimumProperties(e.params.limit);
104+
break;
105+
case 'maxProperties':
106+
message = validationMessagesFormatters.maximumProperties(e.params.limit);
107+
break;
108+
case 'minItems':
109+
message = validationMessagesFormatters.minimumItems(e.params.limit);
110+
break;
111+
case 'maxItems':
112+
message = validationMessagesFormatters.maximumItems(e.params.limit);
113+
break;
114+
case 'minimum':
115+
message = validationMessagesFormatters.minimum(e.params.limit);
116+
break;
117+
case 'maximum':
118+
message = validationMessagesFormatters.maximum(e.params.limit);
119+
break;
120+
case 'enum':
121+
message = validationMessagesFormatters.enum(e.params.allowedValues);
122+
break;
123+
case 'pattern':
124+
pattern = e.params.pattern;
125+
value = get(data, key);
126+
if (pattern === '.+' && !value) {
127+
message = validationMessagesFormatters.presentString();
128+
}
129+
else {
130+
message = validationMessagesFormatters.pattern(e.params.pattern);
131+
}
132+
break;
133+
case 'format':
134+
reason = e.params.format;
135+
// Normalize the key
136+
if (reason === 'date-time') {
137+
reason = 'timestamp';
138+
}
139+
message = (validationMessagesFormatters[reason] || validationMessagesFormatters.invalidFormat)(reason);
140+
break;
141+
}
142+
// No custom message was found, default to input one replacing the starting verb and adding some path info
143+
if (!message.length) {
144+
message = `${e.message.replace(/^should/, 'must')} (${e.keyword})`;
145+
}
146+
// Remove useless quotes
147+
/* istanbul ignore next */
148+
if (key.match(/(?:^['"])(?:[^.]+)(?:['"]$)/)) {
149+
key = key.substring(1, key.length - 1);
150+
}
151+
// Fix empty properties
152+
if (!key) {
153+
key = '$root';
154+
}
155+
key = key.replace(/^\//, '');
156+
errors[key] = message;
157+
}
158+
return { [section]: errors };
159+
}
160+
export function addResponseValidation(route) {
161+
var _a;
162+
if (!((_a = route.schema) === null || _a === void 0 ? void 0 : _a.response)) {
163+
return;
164+
}
165+
const validators = {};
166+
/*
167+
Add these validators to the list of the one to compile once the server is started.
168+
This makes possible to handle shared schemas.
169+
*/
170+
this[kHttpErrorsEnhancedResponseValidations].push([
171+
this,
172+
validators,
173+
Object.entries(route.schema.response)
174+
]);
175+
// Note that this hook is not called for non JSON payloads therefore validation is not possible in such cases
176+
route.preSerialization = async function (request, reply, payload) {
177+
const statusCode = reply.raw.statusCode;
178+
// Never validate error 500
179+
if (statusCode === INTERNAL_SERVER_ERROR) {
180+
return payload;
181+
}
182+
// No validator, it means the HTTP status is not allowed
183+
const validator = validators[statusCode];
184+
if (!validator) {
185+
if (request[kHttpErrorsEnhancedProperties].allowUndeclaredResponses) {
186+
return payload;
187+
}
188+
throw new InternalServerError(validationMessagesFormatters.invalidResponseCode(statusCode));
189+
}
190+
// Now validate the payload
191+
const valid = validator(payload);
192+
if (!valid) {
193+
throw new InternalServerError(validationMessagesFormatters.invalidResponse(statusCode), {
194+
failedValidations: convertValidationErrors('response', payload, validator.errors)
195+
});
196+
}
197+
return payload;
198+
};
199+
}
200+
export function compileResponseValidationSchema() {
201+
// Fix CJS/ESM interoperability
202+
// @ts-expect-error
203+
let AjvConstructor = Ajv;
204+
/* istanbul ignore next */
205+
if (AjvConstructor.default) {
206+
AjvConstructor = AjvConstructor.default;
207+
}
208+
for (const [instance, validators, schemas] of this[kHttpErrorsEnhancedResponseValidations]) {
209+
// @ts-expect-error
210+
const compiler = new AjvConstructor({
211+
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
212+
removeAdditional: false,
213+
useDefaults: true,
214+
coerceTypes: false,
215+
allErrors: true
216+
});
217+
compiler.addSchema(Object.values(instance.getSchemas()));
218+
compiler.addKeyword('example');
219+
for (const [code, schema] of schemas) {
220+
validators[code] = compiler.compile(schema);
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)