Skip to content

Commit 8282167

Browse files
committed
feat: Support shared schema in responses validation.
1 parent 156b45c commit 8282167

File tree

12 files changed

+137
-85
lines changed

12 files changed

+137
-85
lines changed

lib/handlers.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Object.defineProperty(exports, "__esModule", { value: true });
33
exports.handleErrors = exports.handleValidationError = exports.handleNotFoundError = void 0;
44
const http_errors_enhanced_1 = require("http-errors-enhanced");
5+
const interfaces_1 = require("./interfaces");
56
const utils_1 = require("./utils");
67
const validation_1 = require("./validation");
78
function handleNotFoundError(request, reply) {
@@ -25,11 +26,11 @@ function handleErrors(error, request, reply) {
2526
// It is a generic error, handle it
2627
const code = error.code;
2728
if (!('statusCode' in error)) {
28-
if ('validation' in error && ((_a = request.errorProperties) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
29+
if ('validation' in error && ((_a = request[interfaces_1.kHttpErrorsEnhancedProperties]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
2930
// If it is a validation error, convert errors to human friendly format
3031
error = handleValidationError(error, request);
3132
}
32-
else if ((_b = request.errorProperties) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
33+
else if ((_b = request[interfaces_1.kHttpErrorsEnhancedProperties]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
3334
// It is requested to hide the error, just log it and then create a generic one
3435
request.log.error({ error: http_errors_enhanced_1.serializeError(error) });
3536
error = new http_errors_enhanced_1.InternalServerError('An error occurred trying to process your request.');

lib/index.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1414
};
1515
Object.defineProperty(exports, "__esModule", { value: true });
1616
exports.plugin = exports.validationMessagesFormatters = exports.niceJoin = exports.convertValidationErrors = void 0;
17-
const ajv_1 = __importDefault(require("ajv"));
1817
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
1918
const handlers_1 = require("./handlers");
19+
const interfaces_1 = require("./interfaces");
2020
const validation_1 = require("./validation");
2121
__exportStar(require("./handlers"), exports);
2222
__exportStar(require("./interfaces"), exports);
@@ -31,23 +31,17 @@ exports.plugin = fastify_plugin_1.default(function (instance, options, done) {
3131
const convertValidationErrors = (_b = options.convertValidationErrors) !== null && _b !== void 0 ? _b : true;
3232
const convertResponsesValidationErrors = (_c = options.convertResponsesValidationErrors) !== null && _c !== void 0 ? _c : !isProduction;
3333
const allowUndeclaredResponses = (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false;
34-
instance.decorateRequest('errorProperties', {
34+
instance.decorateRequest(interfaces_1.kHttpErrorsEnhancedProperties, {
3535
hideUnhandledErrors,
3636
convertValidationErrors,
3737
allowUndeclaredResponses
3838
});
3939
instance.setErrorHandler(handlers_1.handleErrors);
4040
instance.setNotFoundHandler(handlers_1.handleNotFoundError);
4141
if (convertResponsesValidationErrors) {
42-
instance.decorate('responseValidatorSchemaCompiler', new ajv_1.default({
43-
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
44-
removeAdditional: false,
45-
useDefaults: true,
46-
coerceTypes: false,
47-
allErrors: true,
48-
nullable: true
49-
}));
42+
instance.decorate(interfaces_1.kHttpErrorsEnhancedResponseValidations, []);
5043
instance.addHook('onRoute', validation_1.addResponseValidation);
44+
instance.addHook('onReady', validation_1.compileResponseValidationSchema);
5145
}
5246
done();
5347
}, { name: 'fastify-http-errors-enhanced' });

lib/interfaces.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +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');
5+
exports.kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation');

lib/validation.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
25
Object.defineProperty(exports, "__esModule", { value: true });
3-
exports.addResponseValidation = exports.convertValidationErrors = exports.validationMessagesFormatters = exports.niceJoin = void 0;
6+
exports.compileResponseValidationSchema = exports.addResponseValidation = exports.convertValidationErrors = exports.validationMessagesFormatters = exports.niceJoin = void 0;
7+
const ajv_1 = __importDefault(require("ajv"));
48
const http_errors_enhanced_1 = require("http-errors-enhanced");
9+
const interfaces_1 = require("./interfaces");
510
const utils_1 = require("./utils");
611
function niceJoin(array, lastSeparator = ' and ', separator = ', ') {
712
switch (array.length) {
@@ -166,9 +171,14 @@ function addResponseValidation(route) {
166171
return;
167172
}
168173
const validators = {};
169-
for (const [code, schema] of Object.entries(route.schema.response)) {
170-
validators[code] = this.responseValidatorSchemaCompiler.compile(schema);
171-
}
174+
/*
175+
Add these validators to the list of the one to compile once the server is started.
176+
This makes possible to handle shared schemas.
177+
*/
178+
this[interfaces_1.kHttpErrorsEnhancedResponseValidations].push([
179+
validators,
180+
Object.entries(route.schema.response)
181+
]);
172182
// Note that this hook is not called for non JSON payloads therefore validation is not possible in such cases
173183
route.preSerialization = async function (request, reply, payload) {
174184
const statusCode = reply.raw.statusCode;
@@ -179,7 +189,7 @@ function addResponseValidation(route) {
179189
// No validator, it means the HTTP status is not allowed
180190
const validator = validators[statusCode];
181191
if (!validator) {
182-
if (request.errorProperties.allowUndeclaredResponses) {
192+
if (request[interfaces_1.kHttpErrorsEnhancedProperties].allowUndeclaredResponses) {
183193
return payload;
184194
}
185195
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponseCode(statusCode));
@@ -195,3 +205,20 @@ function addResponseValidation(route) {
195205
};
196206
}
197207
exports.addResponseValidation = addResponseValidation;
208+
function compileResponseValidationSchema() {
209+
const validator = new ajv_1.default({
210+
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
211+
removeAdditional: false,
212+
useDefaults: true,
213+
coerceTypes: false,
214+
allErrors: true,
215+
nullable: true
216+
});
217+
validator.addSchema(Object.values(this.getSchemas()));
218+
for (const [validators, schemas] of this[interfaces_1.kHttpErrorsEnhancedResponseValidations]) {
219+
for (const [code, schema] of schemas) {
220+
validators[code] = validator.compile(schema);
221+
}
222+
}
223+
}
224+
exports.compileResponseValidationSchema = compileResponseValidationSchema;

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, NodeError, RequestSection } from './interfaces'
13+
import { GenericObject, kHttpErrorsEnhancedProperties, 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.errorProperties?.convertValidationErrors) {
39+
if ('validation' in error && request[kHttpErrorsEnhancedProperties]?.convertValidationErrors) {
4040
// If it is a validation error, convert errors to human friendly format
4141
error = handleValidationError(error, request)
42-
} else if (request.errorProperties?.hideUnhandledErrors) {
42+
} else if (request[kHttpErrorsEnhancedProperties]?.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: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import Ajv from 'ajv'
21
import { FastifyError, FastifyInstance, FastifyPluginOptions } from 'fastify'
32
import fastifyPlugin from 'fastify-plugin'
43
import { handleErrors, handleNotFoundError } from './handlers'
5-
import { addResponseValidation } from './validation'
4+
import { kHttpErrorsEnhancedProperties, kHttpErrorsEnhancedResponseValidations } from './interfaces'
5+
import { addResponseValidation, compileResponseValidationSchema } from './validation'
66

77
export * from './handlers'
88
export * from './interfaces'
@@ -16,7 +16,7 @@ export const plugin = fastifyPlugin(
1616
const convertResponsesValidationErrors = options.convertResponsesValidationErrors ?? !isProduction
1717
const allowUndeclaredResponses = options.allowUndeclaredResponses ?? false
1818

19-
instance.decorateRequest('errorProperties', {
19+
instance.decorateRequest(kHttpErrorsEnhancedProperties, {
2020
hideUnhandledErrors,
2121
convertValidationErrors,
2222
allowUndeclaredResponses
@@ -25,19 +25,10 @@ export const plugin = fastifyPlugin(
2525
instance.setNotFoundHandler(handleNotFoundError)
2626

2727
if (convertResponsesValidationErrors) {
28-
instance.decorate(
29-
'responseValidatorSchemaCompiler',
30-
new Ajv({
31-
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
32-
removeAdditional: false,
33-
useDefaults: true,
34-
coerceTypes: false,
35-
allErrors: true,
36-
nullable: true
37-
})
38-
)
28+
instance.decorate(kHttpErrorsEnhancedResponseValidations, [])
3929

4030
instance.addHook('onRoute', addResponseValidation)
31+
instance.addHook('onReady', compileResponseValidationSchema)
4132
}
4233

4334
done()

src/interfaces.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { Ajv, ValidateFunction } from 'ajv'
22

3+
export const kHttpErrorsEnhancedProperties = Symbol('fastify-http-errors-enhanced-properties')
4+
export const kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation')
5+
36
declare module 'fastify' {
47
// eslint-disable-next-line @typescript-eslint/no-unused-vars
58
interface FastifyInstance {
69
responseValidatorSchemaCompiler: Ajv
10+
[kHttpErrorsEnhancedResponseValidations]: Array<[ResponseSchemas, Array<[string, object]>]>
711
}
812

913
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1014
interface FastifyRequest {
11-
errorProperties?: {
15+
[kHttpErrorsEnhancedProperties]?: {
1216
hideUnhandledErrors?: boolean
1317
convertValidationErrors?: boolean
1418
allowUndeclaredResponses?: boolean

src/validation.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import Ajv from 'ajv'
12
import { FastifyInstance, FastifyReply, FastifyRequest, RouteOptions, ValidationResult } from 'fastify'
23
import { InternalServerError, INTERNAL_SERVER_ERROR } from 'http-errors-enhanced'
3-
import { RequestSection, ResponseSchemas, ValidationFormatter, Validations } from './interfaces'
4+
import {
5+
kHttpErrorsEnhancedProperties,
6+
kHttpErrorsEnhancedResponseValidations,
7+
RequestSection,
8+
ResponseSchemas,
9+
ValidationFormatter,
10+
Validations
11+
} from './interfaces'
412
import { get } from './utils'
513

614
export function niceJoin(array: Array<string>, lastSeparator: string = ' and ', separator: string = ', '): string {
@@ -194,9 +202,15 @@ export function addResponseValidation(this: FastifyInstance, route: RouteOptions
194202
}
195203

196204
const validators: ResponseSchemas = {}
197-
for (const [code, schema] of Object.entries(route.schema.response as { [key: string]: object })) {
198-
validators[code] = this.responseValidatorSchemaCompiler.compile(schema)
199-
}
205+
206+
/*
207+
Add these validators to the list of the one to compile once the server is started.
208+
This makes possible to handle shared schemas.
209+
*/
210+
this[kHttpErrorsEnhancedResponseValidations].push([
211+
validators,
212+
Object.entries(route.schema.response as { [key: string]: object })
213+
])
200214

201215
// Note that this hook is not called for non JSON payloads therefore validation is not possible in such cases
202216
route.preSerialization = async function (
@@ -216,7 +230,7 @@ export function addResponseValidation(this: FastifyInstance, route: RouteOptions
216230
const validator = validators[statusCode]
217231

218232
if (!validator) {
219-
if (request.errorProperties!.allowUndeclaredResponses) {
233+
if (request[kHttpErrorsEnhancedProperties]!.allowUndeclaredResponses) {
220234
return payload
221235
}
222236

@@ -235,3 +249,22 @@ export function addResponseValidation(this: FastifyInstance, route: RouteOptions
235249
return payload
236250
}
237251
}
252+
253+
export function compileResponseValidationSchema(this: FastifyInstance): void {
254+
const validator = new Ajv({
255+
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
256+
removeAdditional: false,
257+
useDefaults: true,
258+
coerceTypes: false,
259+
allErrors: true,
260+
nullable: true
261+
})
262+
263+
validator.addSchema(Object.values(this.getSchemas()))
264+
265+
for (const [validators, schemas] of this[kHttpErrorsEnhancedResponseValidations]) {
266+
for (const [code, schema] of schemas) {
267+
validators[code] = validator.compile(schema)
268+
}
269+
}
270+
}

test/index.test.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
UNSUPPORTED_MEDIA_TYPE
1212
} from 'http-errors-enhanced'
1313
import t from 'tap'
14-
import { handleErrors, plugin as fastifyErrorProperties } from '../src'
14+
import { handleErrors, plugin as fastifyHttpErrorsEnhanced } from '../src'
1515

1616
type Test = typeof t
1717
type Callback = () => void
@@ -144,11 +144,6 @@ function routes(instance: FastifyInstance, _options: unknown, done: Callback): v
144144
}
145145

146146
async function buildServer(options: FastifyPluginOptions = {}): Promise<FastifyInstance> {
147-
if (server) {
148-
await server.close()
149-
server = null
150-
}
151-
152147
server = fastify({
153148
ajv: {
154149
customOptions: {
@@ -161,19 +156,13 @@ async function buildServer(options: FastifyPluginOptions = {}): Promise<FastifyI
161156
}
162157
})
163158

164-
server.register(fastifyErrorProperties, options)
159+
server.register(fastifyHttpErrorsEnhanced, options)
165160
server.register(routes)
166-
await server.listen(0)
167161

168162
return server
169163
}
170164

171165
async function buildStandaloneServer(): Promise<FastifyInstance> {
172-
if (standaloneServer) {
173-
await standaloneServer.close()
174-
standaloneServer = null
175-
}
176-
177166
standaloneServer = fastify()
178167

179168
standaloneServer.setErrorHandler(handleErrors)
@@ -203,17 +192,11 @@ async function buildStandaloneServer(): Promise<FastifyInstance> {
203192
}
204193
})
205194

206-
await standaloneServer.listen(0)
207-
208195
return standaloneServer
209196
}
210197

211198
t.test('Plugin', (t: Test) => {
212199
t.test('Handling http-errors', (t: Test) => {
213-
t.afterEach(async () => {
214-
await server!.close()
215-
})
216-
217200
t.test('should correctly return client errors', async (t: Test) => {
218201
await buildServer()
219202

@@ -327,10 +310,6 @@ t.test('Plugin', (t: Test) => {
327310
})
328311

329312
t.test('Handling generic errors', (t: Test) => {
330-
t.afterEach(async () => {
331-
await server!.close()
332-
})
333-
334313
t.test(
335314
'should correctly return generic errors by wrapping them in a 500 http-error, including headers and properties',
336315
async (t: Test) => {
@@ -446,10 +425,6 @@ t.test('Plugin', (t: Test) => {
446425
})
447426

448427
t.test('Handling validation errors', (t: Test) => {
449-
t.afterEach(async () => {
450-
await server!.close()
451-
})
452-
453428
t.test('should validate params', async (t: Test) => {
454429
await buildServer()
455430

@@ -555,11 +530,7 @@ t.test('Plugin', (t: Test) => {
555530
})
556531

557532
t.test('Using standalone error handling', (t: Test) => {
558-
t.afterEach(async () => {
559-
await standaloneServer!.close()
560-
})
561-
562-
t.test('should not return the errorProperties by never masking server side errors', async (t: Test) => {
533+
t.test("should not return the error's properties by masking server side errors", async (t: Test) => {
563534
await buildStandaloneServer()
564535

565536
const response = await standaloneServer!.inject({ method: 'GET', url: '/error/123' })

0 commit comments

Comments
 (0)