Skip to content

Commit 3bfe306

Browse files
committed
feat: Added responseValidatorCustomizer.
1 parent 790b95c commit 3bfe306

16 files changed

+126
-72
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Register as a plugin, optional providing any of the following options:
2525
- `convertValidationErrors`: Convert validation errors to a structured human readable object. Default is `true`.
2626
- `convertResponsesValidationErrors`: Convert response validation errors to a structured human readable object. Default is to enable when `NODE_ENV` environment variable is different from `production`.
2727
- `allowUndeclaredResponses`: When converting response validation errors, allow responses that have no schema defined instead of throwing an error.
28+
- `responseValidatorCustomizer`: A function that receives a Ajv instances before compiling all response schemas. This can be used to add custom keywords, formats and so on.
2829

2930
Once registered, the server will use the plugin handlers for all errors (basically, both `setErrorHandler` and `setNotFoundHandler` are called).
3031

dist/cjs/handlers.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ function handleErrors(error, request, reply) {
2626
// It is a generic error, handle it
2727
const code = error.code;
2828
if (!('statusCode' in error)) {
29-
if ('validation' in error && ((_a = request[interfaces_1.kHttpErrorsEnhancedProperties]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
29+
if ('validation' in error && ((_a = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
3030
// If it is a validation error, convert errors to human friendly format
3131
error = handleValidationError(error, request);
3232
}
33-
else if ((_b = request[interfaces_1.kHttpErrorsEnhancedProperties]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
33+
else if ((_b = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
3434
// It is requested to hide the error, just log it and then create a generic one
3535
request.log.error({ error: http_errors_enhanced_1.serializeError(error) });
3636
error = new http_errors_enhanced_1.InternalServerError('An error occurred trying to process your request.');

dist/cjs/index.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,24 @@ Object.defineProperty(exports, "validationMessagesFormatters", { enumerable: tru
2727
exports.plugin = fastify_plugin_1.default(function (instance, options, done) {
2828
var _a, _b, _c, _d;
2929
const isProduction = process.env.NODE_ENV === 'production';
30-
const hideUnhandledErrors = (_a = options.hideUnhandledErrors) !== null && _a !== void 0 ? _a : isProduction;
31-
const convertValidationErrors = (_b = options.convertValidationErrors) !== null && _b !== void 0 ? _b : true;
32-
const convertResponsesValidationErrors = (_c = options.convertResponsesValidationErrors) !== null && _c !== void 0 ? _c : !isProduction;
33-
const allowUndeclaredResponses = (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false;
34-
instance.decorateRequest(interfaces_1.kHttpErrorsEnhancedProperties, null);
30+
const convertResponsesValidationErrors = (_a = options.convertResponsesValidationErrors) !== null && _a !== void 0 ? _a : !isProduction;
31+
const configuration = {
32+
hideUnhandledErrors: (_b = options.hideUnhandledErrors) !== null && _b !== void 0 ? _b : isProduction,
33+
convertValidationErrors: (_c = options.convertValidationErrors) !== null && _c !== void 0 ? _c : true,
34+
responseValidatorCustomizer: options.responseValidatorCustomizer,
35+
allowUndeclaredResponses: (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false
36+
};
37+
instance.decorate(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
38+
instance.decorateRequest(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
3539
instance.addHook('onRequest', async (request) => {
36-
request[interfaces_1.kHttpErrorsEnhancedProperties] = {
37-
hideUnhandledErrors,
38-
convertValidationErrors,
39-
allowUndeclaredResponses
40-
};
40+
request[interfaces_1.kHttpErrorsEnhancedConfiguration] = configuration;
4141
});
4242
instance.setErrorHandler(handlers_1.handleErrors);
4343
instance.setNotFoundHandler(handlers_1.handleNotFoundError);
4444
if (convertResponsesValidationErrors) {
4545
instance.decorate(interfaces_1.kHttpErrorsEnhancedResponseValidations, []);
4646
instance.addHook('onRoute', validation_1.addResponseValidation);
47-
instance.addHook('onReady', validation_1.compileResponseValidationSchema);
47+
instance.addHook('onReady', validation_1.compileResponseValidationSchema.bind(instance, configuration));
4848
}
4949
done();
5050
}, { name: 'fastify-http-errors-enhanced' });

dist/cjs/interfaces.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use strict";
22
Object.defineProperty(exports, "__esModule", { value: true });
3-
exports.kHttpErrorsEnhancedResponseValidations = exports.kHttpErrorsEnhancedProperties = void 0;
4-
exports.kHttpErrorsEnhancedProperties = Symbol('fastify-http-errors-enhanced-properties');
3+
exports.kHttpErrorsEnhancedResponseValidations = exports.kHttpErrorsEnhancedConfiguration = void 0;
4+
exports.kHttpErrorsEnhancedConfiguration = Symbol('fastify-http-errors-enhanced-configuration');
55
exports.kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation');

dist/cjs/validation.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ function addResponseValidation(route) {
190190
// No validator, it means the HTTP status is not allowed
191191
const validator = validators[statusCode];
192192
if (!validator) {
193-
if (request[interfaces_1.kHttpErrorsEnhancedProperties].allowUndeclaredResponses) {
193+
if (request[interfaces_1.kHttpErrorsEnhancedConfiguration].allowUndeclaredResponses) {
194194
return payload;
195195
}
196196
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponseCode(statusCode));
@@ -206,14 +206,15 @@ function addResponseValidation(route) {
206206
};
207207
}
208208
exports.addResponseValidation = addResponseValidation;
209-
function compileResponseValidationSchema() {
209+
function compileResponseValidationSchema(configuration) {
210210
// Fix CJS/ESM interoperability
211211
// @ts-expect-error
212212
let AjvConstructor = ajv_1.default;
213213
/* istanbul ignore next */
214214
if (AjvConstructor.default) {
215215
AjvConstructor = AjvConstructor.default;
216216
}
217+
const hasCustomizer = typeof configuration.responseValidatorCustomizer === 'function';
217218
for (const [instance, validators, schemas] of this[interfaces_1.kHttpErrorsEnhancedResponseValidations]) {
218219
// @ts-expect-error
219220
const compiler = new AjvConstructor({
@@ -225,6 +226,9 @@ function compileResponseValidationSchema() {
225226
});
226227
compiler.addSchema(Object.values(instance.getSchemas()));
227228
compiler.addKeyword('example');
229+
if (hasCustomizer) {
230+
configuration.responseValidatorCustomizer(compiler);
231+
}
228232
for (const [code, schema] of schemas) {
229233
validators[code] = compiler.compile(schema);
230234
}

dist/mjs/handlers.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { addAdditionalProperties, BadRequestError, InternalServerError, INTERNAL_SERVER_ERROR, messagesByCodes, NotFoundError, serializeError, UnsupportedMediaTypeError } from 'http-errors-enhanced';
2-
import { kHttpErrorsEnhancedProperties } from "./interfaces.mjs";
2+
import { kHttpErrorsEnhancedConfiguration } from "./interfaces.mjs";
33
import { upperFirst } from "./utils.mjs";
44
import { convertValidationErrors, validationMessagesFormatters } from "./validation.mjs";
55
export function handleNotFoundError(request, reply) {
@@ -21,11 +21,11 @@ export function handleErrors(error, request, reply) {
2121
// It is a generic error, handle it
2222
const code = error.code;
2323
if (!('statusCode' in error)) {
24-
if ('validation' in error && ((_a = request[kHttpErrorsEnhancedProperties]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
24+
if ('validation' in error && ((_a = request[kHttpErrorsEnhancedConfiguration]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
2525
// If it is a validation error, convert errors to human friendly format
2626
error = handleValidationError(error, request);
2727
}
28-
else if ((_b = request[kHttpErrorsEnhancedProperties]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
28+
else if ((_b = request[kHttpErrorsEnhancedConfiguration]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
2929
// It is requested to hide the error, just log it and then create a generic one
3030
request.log.error({ error: serializeError(error) });
3131
error = new InternalServerError('An error occurred trying to process your request.');

dist/mjs/index.mjs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
import fastifyPlugin from 'fastify-plugin';
22
import { handleErrors, handleNotFoundError } from "./handlers.mjs";
3-
import { kHttpErrorsEnhancedProperties, kHttpErrorsEnhancedResponseValidations } from "./interfaces.mjs";
3+
import { kHttpErrorsEnhancedConfiguration, kHttpErrorsEnhancedResponseValidations } from "./interfaces.mjs";
44
import { addResponseValidation, compileResponseValidationSchema } from "./validation.mjs";
55
export * from "./handlers.mjs";
66
export * from "./interfaces.mjs";
77
export { convertValidationErrors, niceJoin, validationMessagesFormatters } from "./validation.mjs";
88
export const plugin = fastifyPlugin(function (instance, options, done) {
99
var _a, _b, _c, _d;
1010
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);
11+
const convertResponsesValidationErrors = (_a = options.convertResponsesValidationErrors) !== null && _a !== void 0 ? _a : !isProduction;
12+
const configuration = {
13+
hideUnhandledErrors: (_b = options.hideUnhandledErrors) !== null && _b !== void 0 ? _b : isProduction,
14+
convertValidationErrors: (_c = options.convertValidationErrors) !== null && _c !== void 0 ? _c : true,
15+
responseValidatorCustomizer: options.responseValidatorCustomizer,
16+
allowUndeclaredResponses: (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false
17+
};
18+
instance.decorate(kHttpErrorsEnhancedConfiguration, null);
19+
instance.decorateRequest(kHttpErrorsEnhancedConfiguration, null);
1620
instance.addHook('onRequest', async (request) => {
17-
request[kHttpErrorsEnhancedProperties] = {
18-
hideUnhandledErrors,
19-
convertValidationErrors,
20-
allowUndeclaredResponses
21-
};
21+
request[kHttpErrorsEnhancedConfiguration] = configuration;
2222
});
2323
instance.setErrorHandler(handleErrors);
2424
instance.setNotFoundHandler(handleNotFoundError);
2525
if (convertResponsesValidationErrors) {
2626
instance.decorate(kHttpErrorsEnhancedResponseValidations, []);
2727
instance.addHook('onRoute', addResponseValidation);
28-
instance.addHook('onReady', compileResponseValidationSchema);
28+
instance.addHook('onReady', compileResponseValidationSchema.bind(instance, configuration));
2929
}
3030
done();
3131
}, { name: 'fastify-http-errors-enhanced' });

dist/mjs/interfaces.mjs

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

dist/mjs/validation.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Ajv from 'ajv';
22
import { InternalServerError, INTERNAL_SERVER_ERROR } from 'http-errors-enhanced';
3-
import { kHttpErrorsEnhancedProperties, kHttpErrorsEnhancedResponseValidations } from "./interfaces.mjs";
3+
import { kHttpErrorsEnhancedConfiguration, kHttpErrorsEnhancedResponseValidations } from "./interfaces.mjs";
44
import { get } from "./utils.mjs";
55
export function niceJoin(array, lastSeparator = ' and ', separator = ', ') {
66
switch (array.length) {
@@ -182,7 +182,7 @@ export function addResponseValidation(route) {
182182
// No validator, it means the HTTP status is not allowed
183183
const validator = validators[statusCode];
184184
if (!validator) {
185-
if (request[kHttpErrorsEnhancedProperties].allowUndeclaredResponses) {
185+
if (request[kHttpErrorsEnhancedConfiguration].allowUndeclaredResponses) {
186186
return payload;
187187
}
188188
throw new InternalServerError(validationMessagesFormatters.invalidResponseCode(statusCode));
@@ -197,14 +197,15 @@ export function addResponseValidation(route) {
197197
return payload;
198198
};
199199
}
200-
export function compileResponseValidationSchema() {
200+
export function compileResponseValidationSchema(configuration) {
201201
// Fix CJS/ESM interoperability
202202
// @ts-expect-error
203203
let AjvConstructor = Ajv;
204204
/* istanbul ignore next */
205205
if (AjvConstructor.default) {
206206
AjvConstructor = AjvConstructor.default;
207207
}
208+
const hasCustomizer = typeof configuration.responseValidatorCustomizer === 'function';
208209
for (const [instance, validators, schemas] of this[kHttpErrorsEnhancedResponseValidations]) {
209210
// @ts-expect-error
210211
const compiler = new AjvConstructor({
@@ -216,6 +217,9 @@ export function compileResponseValidationSchema() {
216217
});
217218
compiler.addSchema(Object.values(instance.getSchemas()));
218219
compiler.addKeyword('example');
220+
if (hasCustomizer) {
221+
configuration.responseValidatorCustomizer(compiler);
222+
}
219223
for (const [code, schema] of schemas) {
220224
validators[code] = compiler.compile(schema);
221225
}

src/handlers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
serializeError,
1111
UnsupportedMediaTypeError
1212
} from 'http-errors-enhanced'
13-
import { GenericObject, kHttpErrorsEnhancedProperties, NodeError, RequestSection } from './interfaces'
13+
import { GenericObject, kHttpErrorsEnhancedConfiguration, NodeError, RequestSection } from './interfaces'
1414
import { upperFirst } from './utils'
1515
import { convertValidationErrors, validationMessagesFormatters } from './validation'
1616

@@ -36,10 +36,10 @@ export function handleErrors(error: FastifyError | Error, request: FastifyReques
3636
const code = (error as NodeError).code
3737

3838
if (!('statusCode' in error)) {
39-
if ('validation' in error && request[kHttpErrorsEnhancedProperties]?.convertValidationErrors) {
39+
if ('validation' in error && request[kHttpErrorsEnhancedConfiguration]?.convertValidationErrors) {
4040
// If it is a validation error, convert errors to human friendly format
4141
error = handleValidationError(error, request)
42-
} else if (request[kHttpErrorsEnhancedProperties]?.hideUnhandledErrors) {
42+
} else if (request[kHttpErrorsEnhancedConfiguration]?.hideUnhandledErrors) {
4343
// It is requested to hide the error, just log it and then create a generic one
4444
request.log.error({ error: serializeError(error) })
4545
error = new InternalServerError('An error occurred trying to process your request.')

src/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyRequest } from 'fastify'
22
import fastifyPlugin from 'fastify-plugin'
33
import { handleErrors, handleNotFoundError } from './handlers'
4-
import { kHttpErrorsEnhancedProperties, kHttpErrorsEnhancedResponseValidations } from './interfaces'
4+
import { Configuration, kHttpErrorsEnhancedConfiguration, kHttpErrorsEnhancedResponseValidations } from './interfaces'
55
import { addResponseValidation, compileResponseValidationSchema } from './validation'
66

77
export * from './handlers'
@@ -11,19 +11,20 @@ export { convertValidationErrors, niceJoin, validationMessagesFormatters } from
1111
export const plugin = fastifyPlugin(
1212
function (instance: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void {
1313
const isProduction = process.env.NODE_ENV === 'production'
14-
const hideUnhandledErrors = options.hideUnhandledErrors ?? isProduction
15-
const convertValidationErrors = options.convertValidationErrors ?? true
1614
const convertResponsesValidationErrors = options.convertResponsesValidationErrors ?? !isProduction
17-
const allowUndeclaredResponses = options.allowUndeclaredResponses ?? false
1815

19-
instance.decorateRequest(kHttpErrorsEnhancedProperties, null)
16+
const configuration: Configuration = {
17+
hideUnhandledErrors: options.hideUnhandledErrors ?? isProduction,
18+
convertValidationErrors: options.convertValidationErrors ?? true,
19+
responseValidatorCustomizer: options.responseValidatorCustomizer,
20+
allowUndeclaredResponses: options.allowUndeclaredResponses ?? false
21+
}
22+
23+
instance.decorate(kHttpErrorsEnhancedConfiguration, null)
24+
instance.decorateRequest(kHttpErrorsEnhancedConfiguration, null)
2025

2126
instance.addHook('onRequest', async (request: FastifyRequest) => {
22-
request[kHttpErrorsEnhancedProperties] = {
23-
hideUnhandledErrors,
24-
convertValidationErrors,
25-
allowUndeclaredResponses
26-
}
27+
request[kHttpErrorsEnhancedConfiguration] = configuration
2728
})
2829

2930
instance.setErrorHandler(handleErrors)
@@ -33,7 +34,7 @@ export const plugin = fastifyPlugin(
3334
instance.decorate(kHttpErrorsEnhancedResponseValidations, [])
3435

3536
instance.addHook('onRoute', addResponseValidation)
36-
instance.addHook('onReady', compileResponseValidationSchema)
37+
instance.addHook('onReady', compileResponseValidationSchema.bind(instance, configuration))
3738
}
3839

3940
done()

src/interfaces.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
import Ajv, { ValidateFunction } from 'ajv'
22

3-
export const kHttpErrorsEnhancedProperties = Symbol('fastify-http-errors-enhanced-properties')
3+
export const kHttpErrorsEnhancedConfiguration = Symbol('fastify-http-errors-enhanced-configuration')
44
export const kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation')
55

6+
export interface Configuration {
7+
hideUnhandledErrors?: boolean
8+
convertValidationErrors?: boolean
9+
allowUndeclaredResponses?: boolean
10+
responseValidatorCustomizer?: (ajv: Ajv) => void
11+
}
12+
613
declare module 'fastify' {
7-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
814
interface FastifyInstance {
9-
responseValidatorSchemaCompiler: Ajv
10-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1115
[kHttpErrorsEnhancedResponseValidations]: Array<[FastifyInstance, ResponseSchemas, Array<[string, object]>]>
1216
}
1317

14-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1518
interface FastifyRequest {
16-
[kHttpErrorsEnhancedProperties]?: {
17-
hideUnhandledErrors?: boolean
18-
convertValidationErrors?: boolean
19-
allowUndeclaredResponses?: boolean
20-
}
19+
[kHttpErrorsEnhancedConfiguration]?: Configuration
2120
}
2221
}
2322

0 commit comments

Comments
 (0)