├── .gitignore ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Constraints Directives RFC (draft 1) 2 | *Working Draft - June 2017* 3 | 4 | ## Introduction 5 | GraphQL Constraints Directives is a set of directives that allows you to annotate [GraphQL types](http://facebook.github.io/graphql/#sec-Types). The primary use case for these annotations is the validation of input parameters. However, it may be used by any tooling including documentation, GraphiQL, form-generation, etc. 6 | It is inspired by [JSON Schema](http://json-schema.org/latest/json-schema-validation.html) and reuses as much semantics as possible from it. 7 | 8 | **Note: Reference implementation is coming** 9 | 10 | ## Notes for readers 11 | The issues list for this draft can be found at https://github.com/apis-guru/graphql-constraints-spec/issues 12 | To provide feedback, use this issue tracker, or email the document editors. 13 | 14 | All the examples below will be based on [GraphQL IDL](https://github.com/facebook/graphql/pull/90). 15 | 16 | ## Notation 17 | 18 | - **Constraints Directive** - is a group of related constraints represented as a GraphQL directive 19 | - **Constraint** - Atomic assertion which is represented as arguments and input values of Constraints Directive 20 | - **Instance** - actual **non-null** value of argument, field, input field that is interpreted by the directive. 21 | 22 | **NOTE**: null values are handled by GraphQL natively. All the constraints don't affect nullability. 23 | 24 | ## Multiple constraints 25 | When a Constraint Directive has more than one constraint in it, they should be treated as logical AND between individual constraints. 26 | 27 | ## Applicability 28 | **TBD** 29 | 30 | ## Kinds of Constraints Directives 31 | 32 | In GraphQL there are types (`String`, `Int`, `Float`, `ID`, `Boolean` and various user-defined types) and type wrappers (`List` and `Not-Null`). 33 | 34 | Accordingly, this document describes two kinds of Constraints Directives: `Type Constraints Directives` and `Wrapper Constraints Directives`. 35 | 36 | ## Type Constraints Directives 37 | 38 | ## Coercion 39 | **TBD** 40 | If applied before coercion, possible issue with `ID` type since it accepts both integers and strings. 41 | 42 | ### Applicability 43 | Type Constraints Directives can be applied to the following GraphQL entities: 44 | - Object Field Definition 45 | - Input Object Field Definition 46 | - Object Field Arguments Definition 47 | - Directive Argument Definition 48 | - Scalar Definition 49 | 50 | Note: Only one Type Constraint per entity is allowed. 51 | 52 | If Type Constraint is applied to an entity of `List` wrapper type it describes innermost values of the lists. 53 | 54 | ### Relation to GraphQL scalars 55 | 56 | Type Constraints Directives do not override GraphQL standard scalars semantic and runtime behavior. Moreover, each Type Constraints Directive is compatible only with specific standard scalars 57 | 58 | |Directive\Type | Float | Int | Boolean | String | ID | 59 | | ------------- | ----- | --- | ------- | ------ | -- | 60 | | @numberValue | + | + | - | - | - | 61 | | @stringValue | - | - | - | + | + | 62 | 63 | 64 | Applying a directive to a field definition, an argument definition or an input field definition of an incompatible standard type should result in an error. 65 | 66 | ### @numberValue 67 | 68 | `@numberValue` directive is used to describe possible numeric values. 69 | Instance is valid if it is a numeric value according to the Serialization Format (e.g. JSON) 70 | 71 | #### Constraints 72 | 73 | ##### multipleOf 74 | The value of `multipleOf` MUST be a number, strictly greater than `0`. A numeric instance is valid only if division by this constraint's value results in an integer. 75 | 76 | ##### max 77 | The value of `max` MUST be a number, representing an inclusive upper limit for a numeric instance. A numeric instance is valid only if the instance is less than or exactly equal to `max`. 78 | 79 | ##### min 80 | The value of `min` MUST be a number, representing an inclusive upper limit for a numeric instance. A numeric instance is valid only if the instance is greater than or exactly equal to `min`. 81 | 82 | ##### exclusiveMax 83 | The value of `exclusiveMax` MUST be a number, representing an exclusive upper limit for a numeric instance. A numeric instance is valid only if it is strictly less than (not equal to) `exclusiveMax`. 84 | 85 | ##### exclusiveMin 86 | The value of `exclusiveMin` MUST be a number, representing an exclusive upper limit for a numeric instance. A numeric instance is valid only if it has a value strictly greater than (not equal to) `exclusiveMin`. 87 | 88 | ##### oneOf 89 | The value of this argument MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. 90 | An instance is valid only if its value is equal to one of the elements in this constraint's array value. 91 | 92 | ##### equals 93 | A numeric instance is valid only if its value is equal to the value of the constrain. 94 | 95 | #### Examples 96 | ```graphql 97 | type Foo { 98 | byte:Integer @numberValue( 99 | min: 0 100 | max: 255 101 | ) 102 | } 103 | ``` 104 | *Examples of valid values for `byte` field*: `155`, `255`, `0` 105 | 106 | *Examples of invalid values for `byte` field*: `"string"`, `256`, `-1` 107 | 108 | ```graphql 109 | type Foo { 110 | bitMask: Integer @numberValue( 111 | oneOf: [ 1, 2, 4, 8, 16, 32, 64, 128 ] 112 | ) 113 | } 114 | ``` 115 | *Examples of valid values for `bitMask` field*: `1`, `16`, `128` 116 | 117 | *Examples of invalid values for `bitMask` field*: `"string"`, `3`, `5` 118 | 119 | 120 | ### @stringValue 121 | 122 | `@stringValue` directive is used to describe possible string values. 123 | Instance is valid if it is a string value according to the Serialization Format (e.g. JSON) 124 | 125 | #### Constraints 126 | 127 | ##### maxLength 128 | The value of this constraint MUST be a non-negative integer. 129 | A string instance is valid against this constraint if its length is less than, or equal to `maxLength`. The length of a string instance is defined as the number of its characters. 130 | 131 | ##### minLength 132 | The value of this constraint MUST be a non-negative integer. 133 | A string instance is valid against this constraint if its length is greater than, or equal to `minLength`. The length of a string instance is defined as the number of its characters. 134 | 135 | ##### startsWith 136 | The value of this constraint MUST be a string. An instance is valid if it begins with the characters of the constraint's string. 137 | 138 | ##### endsWith 139 | The value of this constraint MUST be a string. An instance is valid if it ends with the characters of the constraint's string. 140 | 141 | ##### includes 142 | The value of this constraint MUST be a string. An instance is valid if constraint's value may be found within the instance string. 143 | 144 | ##### regex 145 | The value of this constraint MUST be a string. This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. An instance is valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored. 146 | 147 | ##### oneOf 148 | The same as for [oneOf for @numberValue](#numbervalue) 149 | 150 | ##### equals 151 | The same as for [equals for @numberValue](#numbervalue) 152 | 153 | #### Examples 154 | 155 | ```graphql 156 | scalar AlphaNumeric @stringValue( 157 | regex: "^[0-9a-zA-Z]*$" 158 | ) 159 | ``` 160 | *Examples of valid values*: `"foo1"`, `"Apollo13"`, `123test` 161 | 162 | *Examples of invalid values*: `3`, `"dash-dash"`, `admin@example.com` 163 | 164 | ## Wrapper Constraints Directives 165 | 166 | ### Applicability 167 | 168 | Wrapper Constraints Directives can be applied to the following GraphQL entities: 169 | - Object Field Definition 170 | - Input Object Field Definition 171 | - Object Field Arguments Definition 172 | - Directive Argument Definition 173 | 174 | **NOTE**: Wrapper Constraints Directives can't be applied to Scalar Definitions 175 | 176 | Wrapper Constraints Directives do not override GraphQL standard wrappers semantics and runtime behavior. Moreover, each Wrapper Constraints Directive is compatible only with specific GraphQL wrapper. 177 | 178 | Applying a directive to a field definition, an argument definition or an input field definition without matching a wrapper type should result in an error. 179 | 180 | ### @list 181 | `@list` directive is used to describe list values. Instance is valid if it is a list according to the Serialization Format (e.g. JSON) 182 | 183 | #### Constraints 184 | 185 | ##### maxItems 186 | The value of this constraint MUST be a non-negative integer. An instance is valid if only its size is less than, or equal to, the value of this directive. 187 | 188 | ##### minItems 189 | The value of this constraint MUST be a non-negative integer. 190 | An instance is valid against `minItems` if its size is greater than, or equal to, the value of this constraint. 191 | Omitting this constraint has the same behavior as a value of 0. 192 | 193 | ##### uniqueItems 194 | The value of this constraint MUST be a boolean. 195 | If it has boolean value `true`, the instance is valid if all of its elements are unique. 196 | 197 | ##### innerList 198 | This constraint is used to describe constraints of the nested List. The value of this constraint MUST be an object. This may contain fields with the same name as arguments of `@list` directive. Semantic of these fields is the same but applied to the inner list. 199 | 200 | #### Examples 201 | 202 | ```graphql 203 | type Foo { 204 | point3D: [Float] @list( 205 | maxItems: 3, 206 | minItems: 3 207 | ) 208 | } 209 | ``` 210 | *Examples of valid values for `point3D` field*: `[1, 2, 3]`, `[-10, 2.5, 100]` 211 | 212 | *Examples of invalid values for `point3D` field*: `[-1, 0]`, `[-1, 0, 100, 0]` 213 | 214 | 215 | 216 | ```graphql 217 | type Foo { 218 | pointOnScreen: [Float] @list( 219 | maxItems: 2, 220 | minItems: 2 221 | ) @numberValue(min: 0.0) 222 | } 223 | ``` 224 | 225 | *Examples of valid values for `pointOnScreen` field*: `[1, 2.5]`, `[0, 100]` 226 | 227 | *Examples of invalid values for `pointOnScreen` field*: `[-10, 100]`, `[100, -100]`, `[0, 0, 0]` 228 | 229 | ```graphql 230 | type ticTacToe { 231 | board: [[String!]!] @list( 232 | minItems: 3, 233 | maxItems: 3 234 | innerList: { 235 | minItems: 3, 236 | maxItems: 3 237 | } 238 | ) @stringValue(oneOf: [" ","X", "O"]) 239 | } 240 | ``` 241 | 242 | *Examples of valid value for `board` field*: 243 | ``` json 244 | [ 245 | [" ", " ", " "], 246 | [" ", "X", " "], 247 | ["O", " ", " "] 248 | ] 249 | ``` 250 | 251 | *Examples of invalid values*: `[]`, `[[],[],[]]`, `"Empty board"`, 252 | ``` json 253 | [ 254 | [" ", " ", " "], 255 | [" ", "Y", " "], 256 | ["N", " ", " "] 257 | ] 258 | ``` 259 | 260 | ## Error messages 261 | **TBD** 262 | 263 | Some ideas: 264 | 265 | **Variant1**: 266 | Allows to provide a single error for multiple constraints but not intuitive structure 267 | ``` 268 | age: Float @numberValue( 269 | min: 0, 270 | max: 100, 271 | errors: [{ 272 | constraints: [min, max] 273 | message: "Invalid age ${value}, should be between ${min} and ${max} years" 274 | code: "AGE_OUT_OF_RANGE" 275 | }] 276 | ) 277 | ``` 278 | 279 | **Variant2**: 280 | Simpler in terms of DX but some messages duplication 281 | ``` 282 | age: Integer @numberValue( 283 | min: 0, 284 | max: 100, 285 | errors: { 286 | min: { 287 | message: Invalid age ${value}, should be greater than ${min} years", 288 | code: "AGE_LT_MINIMUM" 289 | }, 290 | max: { 291 | message: "Invalid age ${value}, should be between less than {max} years", 292 | code: "AGE_GT_MAXIMUM" 293 | } 294 | } 295 | ) 296 | ``` 297 | 298 | ## Appendix A: Some Examples 299 | 300 | ``` 301 | type Foo { 302 | bar: [Int] @numberValue( 303 | multipleOf: 0.01 304 | ) @list({ 305 | minItems: 1, 306 | maxItems: 3, 307 | uniqueItems: true 308 | }) 309 | } 310 | ``` 311 | 312 | *Examples of valid values for `bar` field*: `[1, 2, 3]`, `[0.01, 0.02]`, `[0.99]` 313 | 314 | *Examples of invalid values for `bar` field*: `[0.999]`, `[]`, `[1, 2, 3, 4]`, `[1.001, 2]`, `[1, 1]` 315 | 316 | ``` 317 | type Query { 318 | allPersons( 319 | first: Integer @numberValue(min: 1, max: 25) 320 | after: String 321 | last: Integer @numberValue(min: 1, max: 25) 322 | before: String 323 | ): [Foo!] 324 | } 325 | ``` 326 | 327 | *Examples of valid values for `first` and `last` input arguments*: `1`, `25`, `10` 328 | 329 | *Examples of invalid values for `first` and `last` input arguments*: `0`, `30` 330 | 331 | # Appendix B: IDL 332 | **TBD** 333 | --------------------------------------------------------------------------------