Skip to content

Commit edc3a92

Browse files
restfulheadPatrick Ruhkopf
authored andcommitted
fix: validation hooks
1 parent a85ab91 commit edc3a92

File tree

5 files changed

+53
-39
lines changed

5 files changed

+53
-39
lines changed

example/src/functions/test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,31 @@ import { AjvOpenApiValidator } from '@restfulhead/azure-functions-nodejs-openapi
66
const openApiContent = fs.readFileSync(`${__dirname}/../../../../test/fixtures/openapi.yaml`, 'utf8')
77
const validator = new AjvOpenApiValidator(yaml.load(openApiContent) as any)
88

9+
const createJsonResponse = (body: any, status: number = 200, headers?: HeadersInit): HttpResponseInit => {
10+
return { body: body === undefined || body === null ? undefined : JSON.stringify(body), status: 400, headers: headers ? headers : { 'Content-Type': 'application/json' } }
11+
}
12+
913
app.hook.preInvocation((preContext: PreInvocationContext) => {
1014
const originalHandler = preContext.functionHandler
11-
const path = preContext.invocationContext.options.trigger.route
15+
const path = '/' + preContext.invocationContext.options.trigger.route
1216

1317
preContext.functionHandler = (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
1418
const method = request.method
1519
const requestBody = request.body
20+
preContext.hookData.requestMethod = method
1621

1722
context.log(`Validating query parameters '${path}', '${method}'`);
1823
const reqParamsValResult = validator.validateQueryParams(path, method, request.query)
1924
if (reqParamsValResult) {
20-
return Promise.resolve({body: reqParamsValResult, status: 400 })
25+
preContext.hookData.requestQueryParameterValidationError = true
26+
return Promise.resolve(createJsonResponse(reqParamsValResult, 400))
2127
}
2228

2329
context.log(`Validating request body for '${path}', '${method}'`);
2430
const reqBodyValResult = validator.validateRequestBody(path, method, requestBody, true)
2531
if (reqBodyValResult) {
26-
return Promise.resolve({body: reqBodyValResult, status: 400 })
32+
preContext.hookData.requestBodyValidationError = true
33+
return Promise.resolve(createJsonResponse(reqBodyValResult, 400))
2734
}
2835

2936
return originalHandler(request, context)
@@ -32,12 +39,12 @@ app.hook.preInvocation((preContext: PreInvocationContext) => {
3239

3340

3441
app.hook.postInvocation((postContext: PostInvocationContext) => {
35-
const path = postContext.invocationContext.options.trigger.route
36-
const method = 'post' // TODO hook data or else? postContext.invocationContext.
42+
const path = '/' + postContext.invocationContext.options.trigger.route
43+
const method = postContext.hookData.requestMethod
3744

38-
if (postContext.result) {
45+
if (postContext.result && !postContext.hookData.requestBodyValidationError && !postContext.hookData.requestQueryParameterValidationError) {
3946
// TODO validate response body
40-
const respBodyValResult = validator.validateResponseBody(path, method, postContext.result, true)
47+
const respBodyValResult = validator.validateResponseBody(path, method, (postContext.result as any)?.status, true)
4148
if (respBodyValResult) {
4249
postContext.result = { body: respBodyValResult, status: 400 }
4350
}
@@ -63,8 +70,8 @@ app.hook.postInvocation(async () => {
6370
await new Promise((resolve) => setTimeout(resolve, 50));
6471
});
6572

66-
app.post('post-hello-world', {
67-
route: 'hello/{world}',
73+
app.put('post-users-uid', {
74+
route: 'users/{uid}',
6875
authLevel: 'anonymous',
6976
handler: postHelloWorld
7077
});

src/ajv-openapi-validator.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,36 +74,36 @@ export class AjvOpenApiValidator implements OpenApiValidator {
7474
// always disable removeAdditional, because it has unexpected results with allOf
7575
this.ajv = new AjvDraft4({ ...ajvOpts, removeAdditional: false })
7676
this.validatorOpts = validatorOpts ? { ...DEFAULT_VALIDATOR_OPTS, ...validatorOpts } : DEFAULT_VALIDATOR_OPTS
77+
if (this.validatorOpts.log == undefined) {
78+
this.validatorOpts.log = () => {}
79+
}
80+
7781
this.initialize(spec)
7882
}
7983

8084
validateResponseBody(
8185
path: string,
8286
method: ValidatorHttpMethod,
8387
status: string,
84-
data: unknown,
85-
strict: boolean): ErrorObj[] | undefined {
86-
const validator = this.responseBodyValidators.find((v) => v.path === path && v.method === method && v.status === status)?.validator
88+
data: unknown): ErrorObj[] | undefined {
89+
const validator = this.responseBodyValidators.find((v) => v.path === path?.toLowerCase() && v.method === method?.toLowerCase() && v.status === status + '')?.validator
8790
if (validator) {
8891
return this.validateBody(validator, data)
89-
} else if (strict) {
90-
throw new Error(`No validator found for '${method} ${path}'`)
92+
} else {
93+
throw new Error(`No response body validator found for '${method}', '${path}', ${status}`)
9194
}
92-
return undefined
9395
}
9496

9597
validateRequestBody(
9698
path: string,
9799
method: ValidatorHttpMethod,
98-
data: unknown,
99-
strict: boolean): ErrorObj[] | undefined {
100-
const validator = this.requestBodyValidators.find((v) => v.path === path && v.method === method)?.validator
100+
data: unknown): ErrorObj[] | undefined {
101+
const validator = this.requestBodyValidators.find((v) => v.path === path?.toLowerCase() && v.method === method?.toLowerCase())?.validator
101102
if (validator) {
102103
return this.validateBody(validator, data)
103-
} else if (strict) {
104-
throw new Error(`No validator found for '${method} ${path}'`)
104+
} else {
105+
throw new Error(`No request body validator found for '${method} ${path}'`)
105106
}
106-
return undefined
107107
}
108108

109109

@@ -124,11 +124,11 @@ export class AjvOpenApiValidator implements OpenApiValidator {
124124
params: URLSearchParams,
125125
strict = true
126126
): ErrorObj[] | undefined {
127-
const parameterDefinitions = this.paramsValidators.filter((p) => p.path === path && p.method === method)
127+
const parameterDefinitions = this.paramsValidators.filter((p) => p.path === path?.toLowerCase() && p.method === method?.toLowerCase())
128128

129129
let errResponse: ErrorObj[] = []
130130
params.forEach((value, key) => {
131-
const paramDefinitionIndex = parameterDefinitions.findIndex((p) => p.param.name === key)
131+
const paramDefinitionIndex = parameterDefinitions.findIndex((p) => p.param.name === key?.toLowerCase())
132132
if (paramDefinitionIndex < 0) {
133133
if (strict) {
134134
errResponse.push({
@@ -204,6 +204,7 @@ export class AjvOpenApiValidator implements OpenApiValidator {
204204
}
205205
}
206206

207+
this.validatorOpts.log(`Adding schema '#/components/schemas/${key}'`)
207208
this.ajv.addSchema(schema, `#/components/schemas/${key}`,);
208209
});
209210
}
@@ -255,9 +256,10 @@ export class AjvOpenApiValidator implements OpenApiValidator {
255256

256257
if (schema) {
257258
const schemaName = `#/paths${path.replace(/[{}]/g, '')}/${method}/requestBody`
259+
this.validatorOpts.log(`Adding request body validator '${path}', '${method}' with schema '${schemaName}'`)
258260
this.ajv.addSchema(schema, schemaName);
259261
const validator = this.ajv.compile({ $ref: schemaName })
260-
this.requestBodyValidators.push({ path, method, validator})
262+
this.requestBodyValidators.push({ path: path.toLowerCase(), method: method.toLowerCase() as ValidatorHttpMethod, validator})
261263
}
262264
}
263265

@@ -294,9 +296,10 @@ export class AjvOpenApiValidator implements OpenApiValidator {
294296

295297
if (schema) {
296298
const schemaName = `#/paths${path.replace(/[{}]/g, '')}/${method}/response/${key}`
299+
this.validatorOpts.log(`Adding response body validator '${path}', '${method}', '${key}' with schema '${schemaName}'`)
297300
this.ajv.addSchema(schema, schemaName);
298301
const validator = this.ajv.compile({ $ref: schemaName })
299-
this.responseBodyValidators.push({ path, method, status: key, validator})
302+
this.responseBodyValidators.push({ path: path.toLowerCase(), method: method.toLowerCase() as ValidatorHttpMethod, status: key, validator})
300303
}
301304
})
302305
}
@@ -334,9 +337,10 @@ export class AjvOpenApiValidator implements OpenApiValidator {
334337
// TODO could also add support for other parameters such as headers here
335338
if (resolvedParam?.in === 'query' && resolvedParam.schema) {
336339
const schemaName = `#/paths${path.replace(/[{}]/g, '')}/${method}/parameters/${resolvedParam.name}`
340+
this.validatorOpts.log(`Adding parameter validator '${path}', '${method}', '${resolvedParam.name}'`)
337341
this.ajv.addSchema(resolvedParam.schema, schemaName);
338342
const validator = this.ajv.compile({ $ref: schemaName })
339-
this.paramsValidators.push({ path, method, param: { name: resolvedParam.name, required: resolvedParam.required, allowEmptyValue: resolvedParam.allowEmptyValue }, validator})
343+
this.paramsValidators.push({ path: path.toLowerCase(), method: method.toLowerCase() as ValidatorHttpMethod, param: { name: resolvedParam.name?.toLowerCase(), required: resolvedParam.required, allowEmptyValue: resolvedParam.allowEmptyValue }, validator})
340344
}
341345
})
342346
}

src/openapi-validator.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,13 @@ export interface ErrorSource {
5050
validateRequestBody(
5151
path: string,
5252
method: ValidatorHttpMethod,
53-
data: unknown,
54-
strict: boolean): ErrorObj[] | undefined
53+
data: unknown): ErrorObj[] | undefined
5554

5655
validateResponseBody(
5756
path: string,
5857
method: ValidatorHttpMethod,
5958
status: string,
60-
data: unknown,
61-
strict: boolean): ErrorObj[] | undefined
59+
data: unknown): ErrorObj[] | undefined
6260

6361
validateQueryParams(
6462
path: string,
@@ -112,12 +110,15 @@ export interface ValidatorOpts {
112110
convertDatesToIsoString?: boolean
113111
/** whether to fail initialization if one of the schema compilations fails */
114112
strict?: boolean
113+
/** function used to log messages */
114+
log?: (message: string) => void
115115
}
116116

117117
export const DEFAULT_VALIDATOR_OPTS: Required<ValidatorOpts> = {
118118
setAdditionalPropertiesToFalse: true,
119119
convertDatesToIsoString: true,
120120
strict: true,
121+
log: (message: string) => { console.log(message) }
121122
}
122123

123124

test/fixtures/openapi.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ paths:
4949
application/json:
5050
schema:
5151
$ref: '#/components/schemas/PutUserResponse'
52+
'400':
53+
$ref: '#/components/responses/ResponseError'
5254
'500':
53-
$ref: '#/components/responses/InternalServerError'
55+
$ref: '#/components/responses/ResponseError'
5456

5557
'/additional-props':
5658
post:
@@ -62,7 +64,7 @@ paths:
6264
required: true
6365
responses:
6466
'500':
65-
$ref: '#/components/responses/InternalServerError'
67+
$ref: '#/components/responses/ResponseError'
6668

6769
'/one-of-example':
6870
get:
@@ -84,11 +86,11 @@ paths:
8486
required: true
8587
responses:
8688
'500':
87-
$ref: '#/components/responses/InternalServerError'
89+
$ref: '#/components/responses/ResponseError'
8890

8991
components:
9092
responses:
91-
InternalServerError:
93+
ResponseError:
9294
description: An unexpected server error occurred
9395
content:
9496
application/json:

test/unit/ajv-validator.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,29 +41,29 @@ describe('The api validator', () => {
4141
})
4242

4343
it('should allow additional props', () => {
44-
expect(validator.validateRequestBody('/additional-props', 'post', { somethingElse: 1, otherAddedProp: '123' }, true)).toEqual(undefined)
44+
expect(validator.validateRequestBody('/additional-props', 'post', { somethingElse: 1, otherAddedProp: '123' })).toEqual(undefined)
4545
})
4646

4747
it('should succeed oneOf A', () => {
4848
const dataWithExtra = { name: 'test', description: 'hello', objType: 'a' }
49-
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra, true)).toEqual(undefined)
49+
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra)).toEqual(undefined)
5050
})
5151

5252
it('should succeed oneOf B', () => {
5353
const dataWithExtra = { somethingElse: 123, objType: 'b' }
54-
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra, true)).toEqual(undefined)
54+
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra)).toEqual(undefined)
5555
})
5656

5757
it('should fail oneOf AB', () => {
5858
const dataWithExtra = { name: 'test', description: 'hello', objType: 'a', somethingElse: 1 }
59-
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra, true)).toEqual(
59+
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra)).toEqual(
6060
[{"code": "Validation-additionalProperties", "source": {"pointer": "#/components/schemas/TestRequestA/additionalProperties"}, "status": 400, "title": "must NOT have additional properties"}]
6161
)
6262
})
6363

6464
it('should fail oneOf missing discriminator', () => {
6565
const dataWithExtra = { name: 'test' }
66-
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra, true)).toEqual(
66+
expect(validator.validateResponseBody('/one-of-example', 'get', '200', dataWithExtra)).toEqual(
6767
[{"code": "Validation-discriminator", "source": {"pointer": "#/discriminator"}, "status": 400, "title": "tag \"objType\" must be string"}]
6868
)
6969
})

0 commit comments

Comments
 (0)