|
1 |
| -const { |
2 |
| - DirectiveLocation, |
3 |
| - GraphQLDirective, |
4 |
| - GraphQLInt, |
5 |
| - GraphQLFloat, |
6 |
| - GraphQLNonNull, |
7 |
| - GraphQLString |
8 |
| -} = require('graphql') |
9 |
| -const { SchemaDirectiveVisitor } = require('graphql-tools') |
| 1 | +const { GraphQLFloat, GraphQLInt, GraphQLString, isNonNullType, isScalarType } = require('graphql') |
| 2 | +const { getDirectives, mapSchema, MapperKind } = require('graphql-tools') |
10 | 3 | const ConstraintStringType = require('./scalars/string')
|
11 | 4 | const ConstraintNumberType = require('./scalars/number')
|
12 | 5 |
|
13 |
| -class ConstraintDirective extends SchemaDirectiveVisitor { |
14 |
| - static getDirectiveDeclaration (directiveName, schema) { |
15 |
| - return new GraphQLDirective({ |
16 |
| - name: directiveName, |
17 |
| - locations: [ |
18 |
| - DirectiveLocation.FIELD_DEFINITION, |
19 |
| - DirectiveLocation.INPUT_FIELD_DEFINITION |
20 |
| - ], |
21 |
| - args: { |
22 |
| - /* Strings */ |
23 |
| - minLength: { type: GraphQLInt }, |
24 |
| - maxLength: { type: GraphQLInt }, |
25 |
| - startsWith: { type: GraphQLString }, |
26 |
| - endsWith: { type: GraphQLString }, |
27 |
| - contains: { type: GraphQLString }, |
28 |
| - notContains: { type: GraphQLString }, |
29 |
| - pattern: { type: GraphQLString }, |
30 |
| - format: { type: GraphQLString }, |
31 |
| - |
32 |
| - /* Numbers (Int/Float) */ |
33 |
| - min: { type: GraphQLFloat }, |
34 |
| - max: { type: GraphQLFloat }, |
35 |
| - exclusiveMin: { type: GraphQLFloat }, |
36 |
| - exclusiveMax: { type: GraphQLFloat }, |
37 |
| - multipleOf: { type: GraphQLFloat } |
38 |
| - } |
39 |
| - }) |
40 |
| - } |
| 6 | +function constraintDirective () { |
| 7 | + const constraintTypes = {} |
41 | 8 |
|
42 |
| - visitInputFieldDefinition (field) { |
43 |
| - this.wrapType(field) |
44 |
| - } |
| 9 | + function getConstraintType (fieldName, type, directiveArgumentMap) { |
| 10 | + // Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ as per graphql-js |
| 11 | + const uniqueTypeName = `${fieldName}_${type.name}_` + Object.entries(directiveArgumentMap) |
| 12 | + .map(([key, value]) => `${key}_${value.toString().replace(/\W/g, '')}`) |
| 13 | + .join('_') |
| 14 | + const key = Symbol.for(uniqueTypeName) |
| 15 | + let constraintType = constraintTypes[key] |
45 | 16 |
|
46 |
| - visitFieldDefinition (field) { |
47 |
| - this.wrapType(field) |
| 17 | + if (constraintType) return constraintType |
| 18 | + |
| 19 | + if (type === GraphQLString) { |
| 20 | + constraintType = new ConstraintStringType(fieldName, uniqueTypeName, type, directiveArgumentMap) |
| 21 | + } else if (type === GraphQLFloat || type === GraphQLInt) { |
| 22 | + constraintType = new ConstraintNumberType(fieldName, uniqueTypeName, type, directiveArgumentMap) |
| 23 | + } else { |
| 24 | + throw new Error(`Not a valid scalar type: ${type.toString()}`) |
| 25 | + } |
| 26 | + |
| 27 | + constraintTypes[key] = constraintType |
| 28 | + |
| 29 | + return constraintType |
48 | 30 | }
|
49 | 31 |
|
50 |
| - wrapType (field) { |
51 |
| - const fieldName = field.astNode.name.value |
52 |
| - |
53 |
| - if (field.type instanceof GraphQLNonNull && field.type.ofType === GraphQLString) { |
54 |
| - field.type = new GraphQLNonNull(new ConstraintStringType(fieldName, field.type.ofType, this.args)) |
55 |
| - } else if (field.type === GraphQLString) { |
56 |
| - field.type = new ConstraintStringType(fieldName, field.type, this.args) |
57 |
| - } else if (field.type instanceof GraphQLNonNull && (field.type.ofType === GraphQLFloat || field.type.ofType === GraphQLInt)) { |
58 |
| - field.type = new GraphQLNonNull(new ConstraintNumberType(fieldName, field.type.ofType, this.args)) |
59 |
| - } else if (field.type === GraphQLFloat || field.type === GraphQLInt) { |
60 |
| - field.type = new ConstraintNumberType(fieldName, field.type, this.args) |
| 32 | + function wrapType (fieldConfig, directiveArgumentMap) { |
| 33 | + let originalType |
| 34 | + |
| 35 | + if (isNonNullType(fieldConfig.type)) { |
| 36 | + originalType = fieldConfig.type.ofType |
| 37 | + } else if (isScalarType(fieldConfig.type)) { |
| 38 | + originalType = fieldConfig.type |
61 | 39 | } else {
|
62 |
| - throw new Error(`Not a scalar type: ${field.type}`) |
| 40 | + throw new Error(`Not a scalar type: ${fieldConfig.type.toString()}`) |
63 | 41 | }
|
| 42 | + |
| 43 | + const fieldName = fieldConfig.astNode.name.value |
| 44 | + |
| 45 | + fieldConfig.type = getConstraintType(fieldName, originalType, directiveArgumentMap) |
64 | 46 | }
|
| 47 | + |
| 48 | + return schema => mapSchema(schema, { |
| 49 | + [MapperKind.FIELD]: (fieldConfig) => { |
| 50 | + const directives = getDirectives(schema, fieldConfig) |
| 51 | + const directiveArgumentMap = directives.constraint |
| 52 | + |
| 53 | + if (directiveArgumentMap) { |
| 54 | + wrapType(fieldConfig, directiveArgumentMap) |
| 55 | + |
| 56 | + return fieldConfig |
| 57 | + } |
| 58 | + } |
| 59 | + }) |
65 | 60 | }
|
66 | 61 |
|
67 |
| -module.exports = ConstraintDirective |
| 62 | +const constraintDirectiveTypeDefs = ` |
| 63 | + directive @constraint( |
| 64 | + # String constraints |
| 65 | + minLength: Int |
| 66 | + maxLength: Int |
| 67 | + startsWith: String |
| 68 | + endsWith: String |
| 69 | + contains: String |
| 70 | + notContains: String |
| 71 | + pattern: String |
| 72 | + format: String |
| 73 | +
|
| 74 | + # Number constraints |
| 75 | + min: Int |
| 76 | + max: Int |
| 77 | + exclusiveMin: Int |
| 78 | + exclusiveMax: Int |
| 79 | + multipleOf: Int |
| 80 | + ) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION` |
| 81 | + |
| 82 | +module.exports = { constraintDirective, constraintDirectiveTypeDefs } |
0 commit comments