Skip to content

Commit 6fea485

Browse files
authored
feat: added support for passing custom formats (#185)
1 parent 2336b40 commit 6fea485

11 files changed

+218
-36
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,32 @@ Supported formats:
426426
- uri
427427
- uuid
428428

429+
#### Custom Format
430+
You can add your own custom formats by passing a `formats` object to the plugin options. See example below.
431+
432+
```@constraint(format: "my-custom-format")```
433+
434+
```js
435+
const formats = {
436+
'my-custom-format': (value) => {
437+
if (value === 'foo') {
438+
return true
439+
}
440+
441+
throw new GraphQLError('Value must be foo')
442+
}
443+
};
444+
445+
// Envelop
446+
createEnvelopQueryValidationPlugin({ formats })
447+
448+
// Apollo 3 Server
449+
createApolloQueryValidationPlugin({ formats })
450+
451+
// Apollo 4 Server
452+
createApollo4QueryValidationPlugin({ formats })
453+
```
454+
429455
### Int/Float
430456
#### min
431457
```@constraint(min: 3)```

apollo4.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { gql } = require('graphql-tag')
99

1010
let currentSchema
1111

12-
function createApollo4QueryValidationPlugin () {
12+
function createApollo4QueryValidationPlugin (options = {}) {
1313
return {
1414
async serverWillStart () {
1515
return {
@@ -30,7 +30,8 @@ function createApollo4QueryValidationPlugin () {
3030
currentSchema,
3131
query,
3232
request.variables,
33-
request.operationName
33+
request.operationName,
34+
options
3435
)
3536

3637
if (errors.length > 0) {

index.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ function constraintDirectiveDocumentation (options) {
171171
})
172172
}
173173

174-
function validateQuery (schema, query, variables, operationName) {
174+
function validateQuery (schema, query, variables, operationName, pluginOptions = {}) {
175175
const typeInfo = new TypeInfo(schema)
176176

177177
const errors = []
@@ -181,17 +181,19 @@ function validateQuery (schema, query, variables, operationName) {
181181
typeInfo,
182182
(error) => errors.push(error)
183183
)
184+
184185
const visitor = new QueryValidationVisitor(context, {
185186
variables,
186-
operationName
187+
operationName,
188+
pluginOptions
187189
})
188190

189191
visit(query, visitWithTypeInfo(typeInfo, visitor))
190192

191193
return errors
192194
}
193195

194-
function createApolloQueryValidationPlugin ({ schema }) {
196+
function createApolloQueryValidationPlugin ({ schema }, options = {}) {
195197
return {
196198
async requestDidStart () {
197199
return ({
@@ -204,7 +206,8 @@ function createApolloQueryValidationPlugin ({ schema }) {
204206
schema,
205207
query,
206208
request.variables,
207-
request.operationName
209+
request.operationName,
210+
options
208211
)
209212
if (errors.length > 0) {
210213
throw errors.map(err => {
@@ -218,10 +221,10 @@ function createApolloQueryValidationPlugin ({ schema }) {
218221
}
219222
}
220223

221-
function createEnvelopQueryValidationPlugin () {
224+
function createEnvelopQueryValidationPlugin (options = {}) {
222225
return {
223226
onExecute ({ args, setResultAndStopExecution }) {
224-
const errors = validateQuery(args.schema, args.document, args.variableValues, args.operationName)
227+
const errors = validateQuery(args.schema, args.document, args.variableValues, args.operationName, options)
225228
if (errors.length > 0) {
226229
setResultAndStopExecution({ errors: errors.map(err => { return new GraphQLError(err.message, err, { code: err.code, field: err.fieldName, context: err.context, exception: err.originalError }) }) })
227230
}

lib/query-validation-visitor.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ module.exports = class QueryValidationVisitor {
135135

136136
const inputObjectTypeDef = getNamedType(valueTypeDef)
137137

138-
validateInputTypeValue(this.context, inputObjectTypeDef, argName, variableName, value, this.currentField, variableName)
138+
validateInputTypeValue(this.context, inputObjectTypeDef, argName, variableName, value, this.currentField, variableName, this.options)
139139
} else if (isListType(valueTypeDef)) {
140140
validateArrayTypeValue(this.context, valueTypeDef, argTypeDef, value, this.currentField, argName, variableName, variableName)
141141
} else {
@@ -144,12 +144,12 @@ module.exports = class QueryValidationVisitor {
144144

145145
const fieldNameForError = variableName || this.currentField.name.value + '.' + argName
146146

147-
validateScalarTypeValue(this.context, this.currentField, argTypeDef, valueTypeDef, value, variableName, argName, fieldNameForError, '')
147+
validateScalarTypeValue(this.context, this.currentField, argTypeDef, valueTypeDef, value, variableName, argName, fieldNameForError, '', this.options)
148148
}
149149
}
150150
}
151151

152-
function validateScalarTypeValue (context, currentQueryField, typeDefWithDirective, valueTypeDef, value, variableName, argName, fieldNameForError, errMessageAt) {
152+
function validateScalarTypeValue (context, currentQueryField, typeDefWithDirective, valueTypeDef, value, variableName, argName, fieldNameForError, errMessageAt, options = {}) {
153153
if (!typeDefWithDirective.astNode) { return }
154154

155155
const directiveArgumentMap = getDirectiveValues(constraintDirectiveTypeDefsObj, typeDefWithDirective.astNode)
@@ -159,7 +159,7 @@ function validateScalarTypeValue (context, currentQueryField, typeDefWithDirecti
159159
const valueDelim = st === GraphQLString ? '"' : ''
160160

161161
try {
162-
getConstraintValidateFn(st)(fieldNameForError, directiveArgumentMap, value)
162+
getConstraintValidateFn(st)(fieldNameForError, directiveArgumentMap, value, options)
163163
} catch (e) {
164164
let error
165165

@@ -175,11 +175,11 @@ function validateScalarTypeValue (context, currentQueryField, typeDefWithDirecti
175175
}
176176
}
177177

178-
function validateInputTypeValue (context, inputObjectTypeDef, argName, variableName, value, currentField, parentNames) {
178+
function validateInputTypeValue (context, inputObjectTypeDef, argName, variableName, value, currentField, parentNames, options = {}) {
179179
if (!inputObjectTypeDef.astNode) { return }
180180

181181
// use new visitor to traverse input object structure
182-
const visitor = new InputObjectValidationVisitor(context, inputObjectTypeDef, argName, variableName, value, currentField, parentNames)
182+
const visitor = new InputObjectValidationVisitor(context, inputObjectTypeDef, argName, variableName, value, currentField, parentNames, options)
183183

184184
visit(inputObjectTypeDef.astNode, visitor)
185185
}
@@ -230,18 +230,18 @@ function validateArrayTypeValue (context, valueTypeDef, typeDefWithDirective, va
230230
const iFieldNameFullIndexed = iFieldNameFull ? `${iFieldNameFull}[${index++}]` : `[${index++}]`
231231

232232
if (isInputObjectType(valueTypeDefArray)) {
233-
validateInputTypeValue(context, valueTypeDefArray, argName, variableName, element, currentField, iFieldNameFullIndexed)
233+
validateInputTypeValue(context, valueTypeDefArray, argName, variableName, element, currentField, iFieldNameFullIndexed, this.options)
234234
} else if (hasNonListValidation) {
235235
const atMessage = ` at "${iFieldNameFullIndexed}"`
236236

237-
validateScalarTypeValue(context, currentField, typeDefWithDirective, valueTypeDef, element, variableName, argName, iFieldNameFullIndexed, atMessage)
237+
validateScalarTypeValue(context, currentField, typeDefWithDirective, valueTypeDef, element, variableName, argName, iFieldNameFullIndexed, atMessage, this.options)
238238
}
239239
})
240240
}
241241
}
242242

243243
class InputObjectValidationVisitor {
244-
constructor (context, inputObjectTypeDef, argName, variableName, value, currentField, parentNames) {
244+
constructor (context, inputObjectTypeDef, argName, variableName, value, currentField, parentNames, options = {}) {
245245
this.context = context
246246
this.argName = argName
247247
this.variableName = variableName
@@ -250,6 +250,7 @@ class InputObjectValidationVisitor {
250250
this.value = value
251251
this.currentField = currentField
252252
this.parentNames = parentNames
253+
this.options = options
253254

254255
this.InputValueDefinition = {
255256
enter: this.onInputValueDefinition
@@ -273,14 +274,14 @@ class InputObjectValidationVisitor {
273274
// nothing to validate
274275
if (!value) return
275276

276-
validateInputTypeValue(this.context, valueTypeDef, this.argName, this.variableName, value, this.currentField, iFieldNameFull)
277+
validateInputTypeValue(this.context, valueTypeDef, this.argName, this.variableName, value, this.currentField, iFieldNameFull, this.options)
277278
} else if (isListType(valueTypeDef)) {
278279
validateArrayTypeValue(this.context, valueTypeDef, iFieldTypeDef, value, this.currentField, this.argName, this.variableName, iFieldNameFull)
279280
} else {
280281
// nothing to validate
281282
if (!value && value !== '' && value !== 0) return
282283

283-
validateScalarTypeValue(this.context, this.currentField, iFieldTypeDef, valueTypeDef, value, this.variableName, this.argName, iFieldNameFull, ` at "${iFieldNameFull}"`)
284+
validateScalarTypeValue(this.context, this.currentField, iFieldTypeDef, valueTypeDef, value, this.variableName, this.argName, iFieldNameFull, ` at "${iFieldNameFull}"`, this.options)
284285
}
285286
}
286287
}

scalars/string.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
11
const { GraphQLScalarType } = require('graphql')
22
const { contains, isLength } = require('validator')
3-
const formats = require('./formats')
3+
const defaultFormats = require('./formats')
44
const ValidationError = require('../lib/error')
55

66
class ConstraintStringType extends GraphQLScalarType {
7-
constructor (fieldName, uniqueTypeName, type, args) {
7+
constructor (fieldName, uniqueTypeName, type, args, options = {}) {
88
super({
99
name: uniqueTypeName,
1010
serialize (value) {
1111
value = type.serialize(value)
1212

13-
validate(fieldName, args, value)
13+
validate(fieldName, args, value, options)
1414

1515
return value
1616
},
1717
parseValue (value) {
1818
value = type.serialize(value)
1919

20-
validate(fieldName, args, value)
20+
validate(fieldName, args, value, options)
2121

2222
return type.parseValue(value)
2323
},
2424
parseLiteral (ast) {
2525
const value = type.parseLiteral(ast)
2626

27-
validate(fieldName, args, value)
27+
validate(fieldName, args, value, options)
2828

2929
return value
3030
}
3131
})
3232
}
3333
}
3434

35-
function validate (fieldName, args, value) {
35+
function validate (fieldName, args, value, options = {}) {
3636
if (args.minLength && !isLength(value, { min: args.minLength })) {
3737
throw new ValidationError(fieldName,
3838
`Must be at least ${args.minLength} characters in length`,
@@ -75,6 +75,8 @@ function validate (fieldName, args, value) {
7575
}
7676

7777
if (args.format) {
78+
const pluginOptions = options.pluginOptions || {}
79+
const formats = { ...defaultFormats, ...(pluginOptions.formats || {}) }
7880
const formatter = formats[args.format]
7981

8082
if (!formatter) {

test/setup-apollo-plugin.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { makeExecutableSchema } = require('@graphql-tools/schema')
44
const request = require('supertest')
55
const { createApolloQueryValidationPlugin, constraintDirectiveTypeDefs } = require('..')
66

7-
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback }) {
7+
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback, pluginOptions = {} }) {
88
let schema = makeExecutableSchema({
99
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
1010
resolvers
@@ -15,9 +15,7 @@ module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreat
1515
}
1616

1717
const plugins = [
18-
createApolloQueryValidationPlugin({
19-
schema
20-
})
18+
createApolloQueryValidationPlugin({ schema }, pluginOptions)
2119
]
2220

2321
const app = express()

test/setup-apollo4-plugin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { json } = require('body-parser')
77
const request = require('supertest')
88
const { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefs } = require('../apollo4')
99

10-
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback }) {
10+
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback, pluginOptions = {} }) {
1111
let schema = makeExecutableSchema({
1212
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
1313
resolvers
@@ -18,7 +18,7 @@ module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreat
1818
}
1919

2020
const plugins = [
21-
createApollo4QueryValidationPlugin()
21+
createApollo4QueryValidationPlugin(pluginOptions)
2222
]
2323

2424
const app = express()

test/setup-envelop-plugin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { createServer } = require('@graphql-yoga/node')
44
const request = require('supertest')
55
const { createEnvelopQueryValidationPlugin, constraintDirectiveTypeDefs } = require('..')
66

7-
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback }) {
7+
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback, pluginOptions = {} }) {
88
let schema = makeExecutableSchema({
99
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
1010
resolvers
@@ -17,7 +17,7 @@ module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreat
1717
const app = express()
1818
const yoga = createServer({
1919
schema,
20-
plugins: [createEnvelopQueryValidationPlugin()],
20+
plugins: [createEnvelopQueryValidationPlugin(pluginOptions)],
2121
graphiql: false
2222
})
2323

test/setup-schema-wrapper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { makeExecutableSchema } = require('@graphql-tools/schema')
44
const request = require('supertest')
55
const { constraintDirectiveTypeDefs, constraintDirective } = require('..')
66

7-
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback }) {
7+
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback, pluginOptions = {} }) {
88
let schema = makeExecutableSchema({
99
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
1010
resolvers

test/setup-validation-rule-express-graphql.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { makeExecutableSchema } = require('@graphql-tools/schema')
44
const request = require('supertest')
55
const { createQueryValidationRule, constraintDirectiveTypeDefs } = require('..')
66

7-
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback }) {
7+
module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreatedCallback, pluginOptions = {} }) {
88
let schema = makeExecutableSchema({
99
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
1010
resolvers
@@ -22,7 +22,8 @@ module.exports = async function ({ typeDefs, formatError, resolvers, schemaCreat
2222
schema,
2323
validationRules: [
2424
createQueryValidationRule({
25-
variables
25+
variables,
26+
pluginOptions
2627
})
2728
]
2829
}))

0 commit comments

Comments
 (0)