Skip to content

Commit 66ab1f0

Browse files
committed
Add support for input object type validation and fix issue with JTD validator
1 parent f4eb746 commit 66ab1f0

13 files changed

+856
-80
lines changed

index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const fp = require('fastify-plugin')
44
const Validation = require('./lib/validation')
55
const { validateOpts } = require('./lib/utils')
6-
const { validationDirective, validationTypeDefs, validationTypes } = require('./lib/directive')
6+
const { validationDirective, validationTypeDefs } = require('./lib/directive')
77
const errors = require('./lib/errors')
88

99
const mercuriusValidation = fp(
@@ -30,7 +30,6 @@ const mercuriusValidation = fp(
3030

3131
mercuriusValidation.graphQLDirective = validationDirective
3232
mercuriusValidation.graphQLTypeDefs = validationTypeDefs
33-
mercuriusValidation.graphQLTypes = validationTypes
3433
mercuriusValidation.errors = errors
3534

3635
module.exports = mercuriusValidation

lib/directive.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ function buildConstraintDirective () {
88

99
const locations = [
1010
DirectiveLocation.ARGUMENT_DEFINITION,
11-
DirectiveLocation.INPUT_FIELD_DEFINITION
11+
DirectiveLocation.INPUT_FIELD_DEFINITION,
12+
DirectiveLocation.INPUT_OBJECT
1213
]
1314

1415
const args = {}
@@ -17,7 +18,6 @@ function buildConstraintDirective () {
1718
boolean: GraphQLBoolean,
1819
string: GraphQLString,
1920
number: GraphQLInt
20-
// TODO: array of strings/numbers - maybe after v1
2121
}
2222

2323
const allowedKeywords = {
@@ -69,13 +69,12 @@ function buildConstraintDirective () {
6969
directives: [directive]
7070
})
7171

72-
return [directive, schema, printSchema(schema)]
72+
return [directive, printSchema(schema)]
7373
}
7474

75-
const [validationDirective, validationTypes, validationTypeDefs] = buildConstraintDirective()
75+
const [validationDirective, validationTypeDefs] = buildConstraintDirective()
7676

7777
module.exports = {
7878
validationDirective,
79-
validationTypes,
8079
validationTypeDefs
8180
}

lib/errors.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ class ValidationError extends Error {
77
super(message)
88
this.name = 'ValidationError'
99
this.code = 'MER_VALIDATION_ERR_FAILED_VALIDATION'
10-
this.extensions = { name: this.name, code: this.code, details }
10+
this.extensions = {
11+
name: this.name,
12+
code: this.code,
13+
details
14+
}
1115
ValidationError.prototype[Symbol.toStringTag] = 'Error'
1216
ValidationError.prototype.toString = function () {
1317
return `${this.name} [${this.code}]: ${this.message}`

lib/symbols.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = {
1212
kBuildValidationSchemaFromDirective: Symbol('build validation schema from directive'),
1313
kSetArgumentValidationSchema: Symbol('set argument validation schema'),
1414
kSetFieldValidationSchema: Symbol('set field validation schema'),
15+
kSetInputObjectTypeValidationSchema: Symbol('set input object type validation schema'),
1516
kJsonSchemaValidator: Symbol('json schema validator'),
1617
kValidationDirective: Symbol('validation directive'),
1718
kGetValidationDirectiveAST: Symbol('get validation directive ast'),

lib/validators/directive-validator.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
kBuildValidationSchema,
1212
kSetFieldValidationSchema,
1313
kSetArgumentValidationSchema,
14+
kSetInputObjectTypeValidationSchema,
1415
kBuildValidationSchemaFromDirective
1516
} = require('../symbols')
1617

@@ -32,6 +33,12 @@ class DirectiveValidator {
3233
return null
3334
}
3435

36+
[kSetInputObjectTypeValidationSchema] (validationSchema, typeName, typeValidation) {
37+
// This is never going to be defined because it is always the first check for a type
38+
validationSchema[typeName] = { __typeValidation: typeValidation }
39+
return validationSchema
40+
}
41+
3542
[kSetFieldValidationSchema] (validationSchema, typeName, fieldName, fieldValidation) {
3643
const typeValidationSchema = validationSchema[typeName]
3744
if (typeof typeValidationSchema === 'object') {
@@ -73,12 +80,19 @@ class DirectiveValidator {
7380
let validationSchema = {}
7481
for (const [typeName, type] of Object.entries(graphQLSchema.getTypeMap())) {
7582
if (!typeName.startsWith('__') && typeof type.getFields === 'function') {
83+
if (isInputObjectType(type)) {
84+
const inputObjectTypeDirectiveAST = this[kGetValidationDirectiveAST](type.astNode)
85+
if (inputObjectTypeDirectiveAST !== null) {
86+
const typeValidationSchema = this[kBuildValidationSchemaFromDirective](inputObjectTypeDirectiveAST)
87+
validationSchema = this[kSetInputObjectTypeValidationSchema](validationSchema, typeName, typeValidationSchema)
88+
}
89+
}
7690
for (const [fieldName, field] of Object.entries(type.getFields())) {
7791
if (typeof field.args !== 'undefined' && Object.keys(field.args).length > 0) {
7892
for (const argument of field.args) {
79-
const directiveAST = this[kGetValidationDirectiveAST](argument.astNode)
80-
if (directiveAST !== null) {
81-
const argumentValidationSchema = this[kBuildValidationSchemaFromDirective](directiveAST)
93+
const argumentDirectiveAST = this[kGetValidationDirectiveAST](argument.astNode)
94+
if (argumentDirectiveAST !== null) {
95+
const argumentValidationSchema = this[kBuildValidationSchemaFromDirective](argumentDirectiveAST)
8296
validationSchema = this[kSetArgumentValidationSchema](
8397
validationSchema,
8498
typeName,
@@ -89,9 +103,9 @@ class DirectiveValidator {
89103
}
90104
}
91105
} else if (isInputObjectType(type)) {
92-
const directiveAST = this[kGetValidationDirectiveAST](field.astNode)
93-
if (directiveAST !== null) {
94-
const fieldValidationSchema = this[kBuildValidationSchemaFromDirective](directiveAST)
106+
const fieldDirectiveAST = this[kGetValidationDirectiveAST](field.astNode)
107+
if (fieldDirectiveAST !== null) {
108+
const fieldValidationSchema = this[kBuildValidationSchemaFromDirective](fieldDirectiveAST)
95109
validationSchema = this[kSetFieldValidationSchema](
96110
validationSchema,
97111
typeName,

lib/validators/function-validator.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class FunctionValidator {
4545
if (typeof schemaArgument !== 'undefined') {
4646
if (typeof argumentFn === 'function') {
4747
override = true
48-
// TODO: create parent validator function that handles different user fn definitions
4948
this[kValidators].set(`${typeName}.${fieldName}.${argumentName}`, argumentFn)
5049
}
5150
} else {

lib/validators/json-schema-validator.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class JSONSchemaValidator extends Validator {
8484
registerValidationSchema (schema, validationSchema) {
8585
// Instantiated here to make sure it is reset after a gateway schema refresh
8686
this[kAjv] = new Ajv({
87+
// TODO: remove in a single commit and discuss
8788
verbose: true,
8889
allErrors: true,
8990
coerceTypes: true,
@@ -97,14 +98,19 @@ class JSONSchemaValidator extends Validator {
9798
// Process each type within the schema
9899
for (const [typeName, schemaType] of Object.entries(schema.getTypeMap())) {
99100
const typeValidation = validationSchema[typeName] || null
100-
const typeValidationSchema = {
101+
let typeValidationSchema = {
101102
$id: `https://mercurius.dev/validation/${typeName}`,
102103
type: 'object',
103104
properties: {}
104105
}
105106

106107
// Process each field for the type
107108
if (!typeName.startsWith('__') && typeof schemaType.getFields === 'function') {
109+
// Handle any input object type validation
110+
if (isInputObjectType(schemaType) && typeValidation !== null && typeof typeValidation.__typeValidation !== 'undefined') {
111+
typeValidationSchema = { ...typeValidation.__typeValidation, ...typeValidationSchema }
112+
}
113+
108114
for (const [fieldName, schemaTypeField] of Object.entries(schemaType.getFields())) {
109115
const fieldValidation = typeValidation !== null ? typeValidation[fieldName] || null : null
110116

lib/validators/jtd-validator.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,18 @@ class JTDValidator extends Validator {
5252
[kBuildInputTypeFieldSchema] (typeName, fieldName, schemaTypeField, fieldValidation) {
5353
const id = `https://mercurius.dev/validation/${typeName}/${fieldName}`
5454
const [fieldType, namedFieldType] = getTypeInfo(schemaTypeField)
55-
return this[kBuildValidationSchema](fieldType, namedFieldType, fieldValidation, id)
55+
const { $id, ...validationSchema } = this[kBuildValidationSchema](fieldType, namedFieldType, fieldValidation, id)
56+
57+
if (Object.keys(validationSchema).length === 0) {
58+
return null
59+
}
60+
return validationSchema
5661
}
5762

5863
registerValidationSchema (schema, validationSchema) {
5964
// Instantiated here to make sure it is reset after a gateway schema refresh
6065
this[kAjv] = new Ajv({
66+
// TODO: remove in a single commit and discuss
6167
verbose: true,
6268
allErrors: true,
6369
// AJV does not yet support type coercion for JTD schemas: https://github.com/ajv-validator/ajv/issues/1724
@@ -74,29 +80,39 @@ class JTDValidator extends Validator {
7480
const typeValidation = validationSchema[typeName] || null
7581
const typeValidationSchema = {
7682
$id: `https://mercurius.dev/validation/${typeName}`,
77-
optionalProperties: {}
83+
optionalProperties: {},
84+
additionalProperties: true
7885
}
7986

8087
// Process each field for the type
8188
if (!typeName.startsWith('__') && typeof schemaType.getFields === 'function') {
82-
for (const [fieldName, schemaTypeField] of Object.entries(schemaType.getFields())) {
83-
const fieldValidation = typeValidation !== null ? typeValidation[fieldName] || null : null
84-
85-
// If the field has arguments, register argument validation
86-
if (typeof schemaTypeField.args !== 'undefined' && Object.keys(schemaTypeField.args).length > 0) {
87-
schemasToRegister.push(this[kBuildArgumentsSchema](typeName, fieldName, schemaTypeField, fieldValidation))
88-
// If the field parent type is an input type, register input object type field validation
89-
} else if (isInputObjectType(schemaType)) {
90-
const { $id, ...fieldValidationSchema } = this[kBuildInputTypeFieldSchema](typeName, fieldName, schemaTypeField, fieldValidation)
91-
if (Object.keys(fieldValidationSchema).length > 0) {
92-
typeValidationSchema.optionalProperties[fieldName] = fieldValidationSchema
89+
// Handle any input object type validation
90+
if (isInputObjectType(schemaType) && typeValidation !== null && typeof typeValidation.__typeValidation !== 'undefined') {
91+
schemasToRegister.push(typeValidation.__typeValidation)
92+
definitions[typeName] = typeValidation.__typeValidation
93+
// Otherwise handle fields as normal
94+
} else {
95+
for (const [fieldName, schemaTypeField] of Object.entries(schemaType.getFields())) {
96+
const fieldValidation = typeValidation !== null ? typeValidation[fieldName] || null : null
97+
98+
// If the field has arguments, register argument validation
99+
if (typeof schemaTypeField.args !== 'undefined' && Object.keys(schemaTypeField.args).length > 0) {
100+
const argumentsSchema = this[kBuildArgumentsSchema](typeName, fieldName, schemaTypeField, fieldValidation)
101+
schemasToRegister.push(argumentsSchema)
102+
// If the field parent type is an input type, register input object type field validation
103+
} else if (isInputObjectType(schemaType)) {
104+
const fieldValidationSchema = this[kBuildInputTypeFieldSchema](typeName, fieldName, schemaTypeField, fieldValidation)
105+
if (fieldValidationSchema !== null) {
106+
typeValidationSchema.optionalProperties[fieldName] = fieldValidationSchema
107+
}
93108
}
94109
}
95-
}
96110

97-
if (isInputObjectType(schemaType)) {
98-
schemasToRegister.push(typeValidationSchema)
99-
definitions[typeName] = { optionalProperties: typeValidationSchema.optionalProperties }
111+
if (isInputObjectType(schemaType)) {
112+
schemasToRegister.push(typeValidationSchema)
113+
const { $id, ...typeValidationSchemaWithoutId } = typeValidationSchema
114+
definitions[typeName] = typeValidationSchemaWithoutId
115+
}
100116
}
101117
}
102118
}

0 commit comments

Comments
 (0)