Skip to content

Commit db8fd47

Browse files
authored
feat: support for list size validation for server plugins based (#103)
fixes #76
1 parent a104397 commit db8fd47

8 files changed

+358
-23
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,16 @@ Ensure value is less than
273273
```@constraint(multipleOf: 10)```
274274
Ensure value is a multiple
275275

276+
### Array/List
277+
278+
#### minItems
279+
```@constraint(minItems: 3)```
280+
Restrict array/List to a minimum length
281+
282+
#### maxItems
283+
```@constraint(maxItems: 3)```
284+
Restrict array/List to a maximum length
285+
276286
### ConstraintDirectiveError
277287
Each validation error throws a `ConstraintDirectiveError`. Combined with a formatError function, this can be used to customise error messages.
278288

lib/query-validation-visitor.js

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,6 @@ module.exports = class QueryValidationVisitor {
116116
const argTypeDef = this.currentrFieldDef.args.find(d => d.name === argName)
117117
const value = valueFromAST(arg.value, argTypeDef.type, this.variableValues)
118118

119-
// nothing to validate
120-
if (!value) return
121-
122119
let variableName
123120

124121
if (arg.value.kind === Kind.VARIABLE) variableName = arg.value.name.value
@@ -128,12 +125,18 @@ module.exports = class QueryValidationVisitor {
128125
if (isNonNullType(valueTypeDef)) valueTypeDef = valueTypeDef.ofType
129126

130127
if (isInputObjectType(valueTypeDef)) {
128+
// nothing to validate
129+
if (!value) return
130+
131131
const inputObjectTypeDef = getNamedType(valueTypeDef)
132132

133133
validateInputTypeValue(this.context, inputObjectTypeDef, argName, variableName, value, this.currentField, variableName)
134134
} else if (isListType(valueTypeDef)) {
135135
validateArrayTypeValue(this.context, valueTypeDef, argTypeDef, value, this.currentField, argName, variableName, variableName)
136136
} else {
137+
// nothing to validate
138+
if (!value) return
139+
137140
const fieldNameForError = variableName || this.currentField.name.value + '.' + argName
138141

139142
validateScalarTypeValue(this.context, this.currentField, argTypeDef, valueTypeDef, value, variableName, argName, fieldNameForError, '')
@@ -177,17 +180,44 @@ function validateArrayTypeValue (context, valueTypeDef, typeDefWithDirective, va
177180

178181
if (isNonNullType(valueTypeDefArray)) valueTypeDefArray = valueTypeDefArray.ofType
179182

180-
value.forEach((element, index) => {
181-
const iFieldNameFullIndexed = iFieldNameFull ? `${iFieldNameFull}[${index++}]` : `[${index++}]`
183+
// Validate array/list size
184+
const directiveArgumentMap = getDirectiveValues(constraintDirectiveTypeDefsObj, typeDefWithDirective.astNode)
185+
186+
if (directiveArgumentMap) {
187+
let errMessageBase
182188

183-
if (isInputObjectType(valueTypeDefArray)) {
184-
validateInputTypeValue(context, valueTypeDefArray, argName, variableName, element, currentField, iFieldNameFullIndexed)
189+
if (variableName) {
190+
errMessageBase = `Variable "$${variableName}" at "${iFieldNameFull}" `
185191
} else {
186-
const atMessage = ` at "${iFieldNameFullIndexed}"`
192+
errMessageBase = `Argument "${argName}" of "${currentField.name.value}" `
193+
}
187194

188-
validateScalarTypeValue(context, currentField, typeDefWithDirective, valueTypeDef, element, variableName, argName, iFieldNameFullIndexed, atMessage)
195+
if (directiveArgumentMap.minItems && (!value || value.length < directiveArgumentMap.minItems)) {
196+
context.reportError(new ValidationError(iFieldNameFull,
197+
errMessageBase + `must be at least ${directiveArgumentMap.minItems} in length`,
198+
[{ arg: 'minItems', value: directiveArgumentMap.minItems }]))
199+
}
200+
if (directiveArgumentMap.maxItems && value && value.length > directiveArgumentMap.maxItems) {
201+
context.reportError(new ValidationError(iFieldNameFull,
202+
errMessageBase + `must be no more than ${directiveArgumentMap.maxItems} in length`,
203+
[{ arg: 'maxItems', value: directiveArgumentMap.maxItems }]))
189204
}
190-
})
205+
}
206+
207+
// Validate array content
208+
if (value) {
209+
value.forEach((element, index) => {
210+
const iFieldNameFullIndexed = iFieldNameFull ? `${iFieldNameFull}[${index++}]` : `[${index++}]`
211+
212+
if (isInputObjectType(valueTypeDefArray)) {
213+
validateInputTypeValue(context, valueTypeDefArray, argName, variableName, element, currentField, iFieldNameFullIndexed)
214+
} else {
215+
const atMessage = ` at "${iFieldNameFullIndexed}"`
216+
217+
validateScalarTypeValue(context, currentField, typeDefWithDirective, valueTypeDef, element, variableName, argName, iFieldNameFullIndexed, atMessage)
218+
}
219+
})
220+
}
191221
}
192222

193223
class InputObjectValidationVisitor {
@@ -213,20 +243,24 @@ class InputObjectValidationVisitor {
213243
const iFieldNameFull = (this.parentNames ? this.parentNames + '.' + iFieldName : iFieldName)
214244
const value = this.value[iFieldName]
215245

216-
if (value) {
217-
let valueTypeAst = node.type
246+
let valueTypeAst = node.type
218247

219-
if (valueTypeAst.kind === Kind.NON_NULL_TYPE) { valueTypeAst = valueTypeAst.type }
248+
if (valueTypeAst.kind === Kind.NON_NULL_TYPE) { valueTypeAst = valueTypeAst.type }
220249

221-
const valueTypeDef = typeFromAST(this.context.getSchema(), valueTypeAst)
250+
const valueTypeDef = typeFromAST(this.context.getSchema(), valueTypeAst)
222251

223-
if (isInputObjectType(valueTypeDef)) {
224-
validateInputTypeValue(this.context, valueTypeDef, this.argName, this.variableName, value, this.currentField, iFieldNameFull)
225-
} else if (isListType(valueTypeDef)) {
226-
validateArrayTypeValue(this.context, valueTypeDef, iFieldTypeDef, value, this.currentField, this.argName, this.variableName, iFieldNameFull)
227-
} else {
228-
validateScalarTypeValue(this.context, this.currentField, iFieldTypeDef, valueTypeDef, value, this.variableName, this.argName, iFieldNameFull, ` at "${iFieldNameFull}"`)
229-
}
252+
if (isInputObjectType(valueTypeDef)) {
253+
// nothing to validate
254+
if (!value) return
255+
256+
validateInputTypeValue(this.context, valueTypeDef, this.argName, this.variableName, value, this.currentField, iFieldNameFull)
257+
} else if (isListType(valueTypeDef)) {
258+
validateArrayTypeValue(this.context, valueTypeDef, iFieldTypeDef, value, this.currentField, this.argName, this.variableName, iFieldNameFull)
259+
} else {
260+
// nothing to validate
261+
if (!value) return
262+
263+
validateScalarTypeValue(this.context, this.currentField, iFieldTypeDef, valueTypeDef, value, this.variableName, this.argName, iFieldNameFull, ` at "${iFieldNameFull}"`)
230264
}
231265
}
232266
}

lib/type-defs.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ const constraintDirectiveTypeDefs = /* GraphQL */`
2424
exclusiveMin: Float
2525
exclusiveMax: Float
2626
multipleOf: Float
27+
28+
# Array/List size constraints
29+
minItems: Int
30+
maxItems: Int
31+
32+
# Shared for Schema wrapper
2733
uniqueTypeName: String
34+
2835
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION | ARGUMENT_DEFINITION`
2936

3037
const constraintDirectiveTypeDefsObj = new GraphQLDirective({
@@ -75,7 +82,15 @@ const constraintDirectiveTypeDefsObj = new GraphQLDirective({
7582
type: GraphQLFloat
7683
},
7784

78-
// Shared
85+
// Array/List size constraints
86+
minItems: {
87+
type: GraphQLInt
88+
},
89+
maxItems: {
90+
type: GraphQLInt
91+
},
92+
93+
// Shared for Schema wrapper
7994
uniqueTypeName: {
8095
type: GraphQLString
8196
}

0 commit comments

Comments
 (0)