├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── float.js ├── index.js └── int.js └── test ├── float.js ├── int.js ├── mocha.opts └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [ 4 | "add-module-exports" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "__SERVER__": true, 8 | "__CLIENT__": true, 9 | "__TEST__": true, 10 | "__DEV__": true, 11 | "__PROD__": true, 12 | "__STAGING__": true 13 | }, 14 | "ecmaFeatures": { 15 | "arrowFunctions": true, 16 | "binaryLiterals": true, 17 | "blockBindings": true, 18 | "classes": true, 19 | "defaultParams": true, 20 | "destructuring": true, 21 | "forOf": true, 22 | "generators": true, 23 | "modules": true, 24 | "objectLiteralComputedProperties": true, 25 | "objectLiteralShorthandMethods": true, 26 | "objectLiteralShorthandProperties": true, 27 | "octalLiterals": true, 28 | "regexUFlag": true, 29 | "regexYFlag": true, 30 | "spread": true, 31 | "superInFunctions": true, 32 | "templateStrings": true, 33 | "unicodeCodePointEscapes": true, 34 | "jsx": true 35 | }, 36 | "parser": "babel-eslint", 37 | "rules": { 38 | "prefer-reflect": [ 39 | 0, 40 | { 41 | "exceptions": [ 42 | "apply", 43 | "call", 44 | "delete" 45 | ] 46 | } 47 | ], 48 | "babel/new-cap": 2, 49 | "no-return-assign": 2, 50 | "no-invalid-this": 0, 51 | "no-void": 2, 52 | "one-var": [2, "never"], 53 | "react/jsx-closing-bracket-location": 0, 54 | "no-undef": 2, 55 | "max-nested-callbacks": [ 56 | 2, 57 | 3 58 | ], 59 | "no-empty": 2, 60 | "no-loop-func": 2, 61 | "keyword-spacing": 2, 62 | "babel/object-shorthand": [ 63 | 2, 64 | "always" 65 | ], 66 | "wrap-iife": [ 67 | 2, 68 | "inside" 69 | ], 70 | "valid-typeof": 2, 71 | "react/jsx-no-literals": 2, 72 | "handle-callback-err": 2, 73 | "operator-linebreak": [2, "after"], 74 | "no-label-var": 2, 75 | "no-process-env": 2, 76 | "no-irregular-whitespace": 2, 77 | "block-spacing": 2, 78 | "padded-blocks": [ 79 | 2, 80 | "never" 81 | ], 82 | "react/jsx-pascal-case": 2, 83 | "no-empty-pattern": 2, 84 | "radix": 2, 85 | "no-undefined": 0, 86 | "semi-spacing": 2, 87 | "eqeqeq": [ 88 | 2, 89 | "allow-null" 90 | ], 91 | "no-negated-condition": 2, 92 | "require-yield": 2, 93 | "new-cap": 2, 94 | "no-const-assign": 2, 95 | "no-bitwise": 2, 96 | "dot-notation": 2, 97 | "camelcase": 2, 98 | "prefer-const": 2, 99 | "no-negated-in-lhs": 2, 100 | "prefer-arrow-callback": 2, 101 | "no-extra-bind": 2, 102 | "react/prefer-es6-class": 2, 103 | "no-sequences": 2, 104 | "babel/generator-star-spacing": 2, 105 | "comma-dangle": [ 106 | 2, 107 | "always-multiline" 108 | ], 109 | "no-spaced-func": 2, 110 | "react/require-extension": 2, 111 | "no-labels": 2, 112 | "no-unreachable": 2, 113 | "no-eval": 2, 114 | "react/no-did-mount-set-state": 2, 115 | "no-unneeded-ternary": 2, 116 | "no-process-exit": 2, 117 | "no-empty-character-class": 2, 118 | "constructor-super": 2, 119 | "no-dupe-class-members": 2, 120 | "strict": [ 121 | 2, 122 | "never" 123 | ], 124 | "no-case-declarations": 2, 125 | "array-bracket-spacing": 2, 126 | "react/no-set-state": 2, 127 | "block-scoped-var": 2, 128 | "arrow-body-style": 2, 129 | "space-in-parens": 2, 130 | "no-confusing-arrow": 2, 131 | "no-control-regex": 2, 132 | "consistent-return": 2, 133 | "no-console": 2, 134 | "comma-spacing": 2, 135 | "no-redeclare": 2, 136 | "computed-property-spacing": 2, 137 | "no-invalid-regexp": 2, 138 | "use-isnan": 2, 139 | "no-new-require": 2, 140 | "indent": [ 141 | 2, 142 | 2 143 | ], 144 | "react/react-in-jsx-scope": 2, 145 | "no-native-reassign": 2, 146 | "no-func-assign": 2, 147 | "max-len": [ 148 | 2, 149 | 120, 150 | 4, 151 | { 152 | "ignoreUrls": true 153 | } 154 | ], 155 | "no-shadow": [ 156 | 2, 157 | { 158 | "builtinGlobals": true 159 | } 160 | ], 161 | "no-mixed-requires": 2, 162 | "react/no-did-update-set-state": 2, 163 | "react/jsx-uses-react": 2, 164 | "max-statements": [ 165 | 2, 166 | 20 167 | ], 168 | "space-unary-ops": [ 169 | 2, 170 | { 171 | "words": true, 172 | "nonwords": false 173 | } 174 | ], 175 | "no-lone-blocks": 2, 176 | "no-debugger": 2, 177 | "arrow-parens": [ 178 | 2, 179 | "always" 180 | ], 181 | "space-before-blocks": [ 182 | 2, 183 | "always" 184 | ], 185 | "no-implied-eval": 2, 186 | "no-useless-concat": 2, 187 | "no-multi-spaces": 2, 188 | "curly": [2, "multi-line"], 189 | "no-extra-boolean-cast": 2, 190 | "space-infix-ops": 2, 191 | "babel/no-await-in-loop": 2, 192 | "react/sort-comp": 2, 193 | "react/jsx-no-undef": 2, 194 | "no-multiple-empty-lines": [ 195 | 2, 196 | { 197 | "max": 2 198 | } 199 | ], 200 | "semi": 2, 201 | "no-param-reassign": 0, 202 | "no-cond-assign": 2, 203 | "no-dupe-keys": 2, 204 | "import/named": 0, 205 | "max-params": [ 206 | 2, 207 | 4 208 | ], 209 | "linebreak-style": 2, 210 | "react/jsx-sort-props": [ 211 | 0, 212 | { 213 | "shorthandFirst": true, 214 | "callbacksLast": true 215 | } 216 | ], 217 | "no-octal-escape": 2, 218 | "no-this-before-super": 2, 219 | "no-alert": 2, 220 | "react/jsx-no-duplicate-props": [ 221 | 2, 222 | { 223 | "ignoreCase": true 224 | } 225 | ], 226 | "no-unused-expressions": 2, 227 | "react/jsx-sort-prop-types": 0, 228 | "no-class-assign": 2, 229 | "spaced-comment": 2, 230 | "no-path-concat": 2, 231 | "prefer-spread": 2, 232 | "no-self-compare": 2, 233 | "guard-for-in": 2, 234 | "no-nested-ternary": 2, 235 | "no-multi-str": 2, 236 | "react/jsx-key": 1, 237 | "import/namespace": 2, 238 | "no-warning-comments": 1, 239 | "no-delete-var": 2, 240 | "babel/arrow-parens": [ 241 | 2, 242 | "always" 243 | ], 244 | "no-with": 2, 245 | "no-extra-parens": 2, 246 | "no-trailing-spaces": 2, 247 | "import/no-unresolved": 1, 248 | "no-obj-calls": 2, 249 | "accessor-pairs": 2, 250 | "yoda": [ 251 | 2, 252 | "never", 253 | { 254 | "exceptRange": true 255 | } 256 | ], 257 | "no-continue": 1, 258 | "react/no-unknown-property": 2, 259 | "no-new": 2, 260 | "object-curly-spacing": 2, 261 | "react/jsx-curly-spacing": [ 262 | 2, 263 | "never" 264 | ], 265 | "jsx-quotes": 2, 266 | "react/no-direct-mutation-state": 2, 267 | "key-spacing": 2, 268 | "no-underscore-dangle": [ 269 | 2, 270 | { "allowAfterThis": true } 271 | ], 272 | "new-parens": 2, 273 | "no-mixed-spaces-and-tabs": 2, 274 | "no-floating-decimal": 2, 275 | "operator-assignment": [ 276 | 2, 277 | "always" 278 | ], 279 | "no-shadow-restricted-names": 2, 280 | "no-use-before-define": [ 281 | 2, 282 | "nofunc" 283 | ], 284 | "no-useless-call": 2, 285 | "no-caller": 2, 286 | "quotes": [ 287 | 2, 288 | "single", 289 | "avoid-escape" 290 | ], 291 | "react/jsx-handler-names": [ 292 | 1, 293 | { 294 | "eventHandlerPrefix": "handle", 295 | "eventHandlerPropPrefix": "on" 296 | } 297 | ], 298 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 299 | "no-unused-vars": 2, 300 | "import/default": 1, 301 | "no-lonely-if": 2, 302 | "no-extra-semi": 2, 303 | "prefer-template": 2, 304 | "react/forbid-prop-types": 1, 305 | "react/self-closing-comp": 2, 306 | "no-else-return": 2, 307 | "react/jsx-max-props-per-line": [ 308 | 2, 309 | { 310 | "maximum": 3 311 | } 312 | ], 313 | "no-dupe-args": 2, 314 | "no-new-object": 2, 315 | "callback-return": 2, 316 | "no-new-wrappers": 2, 317 | "comma-style": 2, 318 | "no-script-url": 2, 319 | "consistent-this": 2, 320 | "react/wrap-multilines": 0, 321 | "dot-location": [ 322 | 2, 323 | "property" 324 | ], 325 | "no-implicit-coercion": 2, 326 | "max-depth": [ 327 | 2, 328 | 4 329 | ], 330 | "babel/object-curly-spacing": [ 331 | 2, 332 | "never" 333 | ], 334 | "no-array-constructor": 2, 335 | "no-iterator": 2, 336 | "react/jsx-no-bind": 2, 337 | "sort-vars": 2, 338 | "no-var": 2, 339 | "no-sparse-arrays": 2, 340 | "space-before-function-paren": [ 341 | 2, 342 | "never" 343 | ], 344 | "no-throw-literal": 2, 345 | "no-proto": 2, 346 | "default-case": 2, 347 | "no-inner-declarations": 2, 348 | "react/jsx-indent-props": [ 349 | 2, 350 | 2 351 | ], 352 | "no-new-func": 2, 353 | "object-shorthand": 2, 354 | "no-ex-assign": 2, 355 | "no-unexpected-multiline": 2, 356 | "no-undef-init": 2, 357 | "no-duplicate-case": 2, 358 | "no-fallthrough": 2, 359 | "no-catch-shadow": 2, 360 | "import/export": 2, 361 | "no-constant-condition": 2, 362 | "complexity": [ 363 | 2, 364 | 25 365 | ], 366 | "react/jsx-boolean-value": [ 367 | 2, 368 | "never" 369 | ], 370 | "valid-jsdoc": 2, 371 | "no-extend-native": 2, 372 | "react/prop-types": 2, 373 | "no-regex-spaces": 2, 374 | "react/no-multi-comp": 2, 375 | "no-octal": 2, 376 | "arrow-spacing": 2, 377 | "quote-props": [ 378 | 2, 379 | "as-needed" 380 | ], 381 | "no-div-regex": 2, 382 | "react/jsx-uses-vars": 2, 383 | "react/no-danger": 1 384 | }, 385 | "settings": { 386 | "ecmascript": 6, 387 | "jsx": true, 388 | "import/parser": "babel-eslint", 389 | "import/ignore": [ 390 | "node_modules", 391 | "\\.scss$" 392 | ], 393 | "import/resolve": { 394 | "moduleDirectory": [ 395 | "node_modules" 396 | ] 397 | } 398 | }, 399 | "plugins": [ 400 | "react", 401 | "import", 402 | "babel" 403 | ] 404 | } 405 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Compiled source 40 | lib 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | .gitignore 4 | .npmignore 5 | .nyc_output 6 | .travis.yml 7 | coverage 8 | src 9 | test 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "5.1" 5 | - "4" 6 | - "4.2" 7 | - "4.1" 8 | - "4.0" 9 | - "0.12" 10 | - "0.11" 11 | - "iojs" 12 | after_success: npm run coverage 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joon Ho Cho 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 | # !!!Checkout new TypeScript rewrite version with some breaking changes 2 | [joonhocho/graphql-scalar](https://github.com/joonhocho/graphql-scalar) 3 | 4 | # graphql-input-number 5 | [![Build Status](https://travis-ci.org/joonhocho/graphql-input-number.svg?branch=master)](https://travis-ci.org/joonhocho/graphql-input-number) 6 | [![Coverage Status](https://coveralls.io/repos/github/joonhocho/graphql-input-number/badge.svg?branch=master)](https://coveralls.io/github/joonhocho/graphql-input-number?branch=master) 7 | [![npm version](https://badge.fury.io/js/graphql-input-number.svg)](https://badge.fury.io/js/graphql-input-number) 8 | [![Dependency Status](https://david-dm.org/joonhocho/graphql-input-number.svg)](https://david-dm.org/joonhocho/graphql-input-number) 9 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) 10 | 11 | A configurable custom input number type for GraphQL with sanitization and validation. 12 | 13 | Checkout [graphql-input-string](https://github.com/joonhocho/graphql-input-string) for validating string inputs. 14 | 15 | 16 | ### Install 17 | ``` 18 | npm install --save graphql-input-number 19 | ``` 20 | 21 | 22 | ### Usage 23 | ```javascript 24 | import { 25 | GraphQLInputInt, 26 | GraphQLInputFloat, 27 | } from 'graphql-input-number'; 28 | 29 | const argType = GraphQLInputInt({ 30 | name: 'OneToNineInt', 31 | min: 1, 32 | max: 9, 33 | }); 34 | 35 | new GraphQLObjectType({ 36 | name: 'Query', 37 | fields: { 38 | input: { 39 | type: GraphQLInt, 40 | args: { 41 | number: { 42 | type: argType, 43 | }, 44 | }, 45 | resolve: (_, {number}) => { 46 | 47 | // 'number' IS AN INT BETWEEN 1 to 9. 48 | 49 | }; 50 | }, 51 | }, 52 | }); 53 | ``` 54 | 55 | ### Options 56 | ```javascript 57 | GraphQLInputInt({ 58 | // Type name. 59 | // [REQUIRED] 60 | name: string = null, 61 | 62 | // Sanitize function that is called at the end of sanitzation phase and before 63 | // validation phase. 64 | sanitize: ((number) => number) = null, 65 | 66 | // Minimum value allowed (inclusive). 67 | min: number = null, 68 | 69 | // Maximum value allowed (inclusive). 70 | max: number = null, 71 | 72 | // Test function that is called at the end of validation phase. 73 | test: ((number) => boolean) = null, 74 | 75 | // Custom error handler. 76 | // May throw an error or return a value. 77 | // If a value is returned, it will become the final value. 78 | error: ErrorHandler = () => throw GraphQLError, 79 | 80 | // Parse function that is called after validation phase before returning a 81 | // value. 82 | // May throw an error or return a value. 83 | parse: ((number) => any) = null, 84 | 85 | // If you want to pass some config to type constructor, simply add them here. 86 | // For example, 87 | description: string, 88 | }); 89 | 90 | GraphQLInputFloat({ 91 | ...same as GraphQLInputInt 92 | }); 93 | 94 | 95 | type ErrorInfo = { 96 | type: string, 97 | value: number, 98 | message: ?string, 99 | ast: ?Ast, 100 | ...args, 101 | }; 102 | 103 | 104 | type ErrorHandler = (ErrorInfo) => any; 105 | ``` 106 | 107 | 108 | ### License 109 | ``` 110 | The MIT License (MIT) 111 | 112 | Copyright (c) 2016 Joon Ho Cho 113 | 114 | Permission is hereby granted, free of charge, to any person obtaining a copy 115 | of this software and associated documentation files (the "Software"), to deal 116 | in the Software without restriction, including without limitation the rights 117 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 118 | copies of the Software, and to permit persons to whom the Software is 119 | furnished to do so, subject to the following conditions: 120 | 121 | The above copyright notice and this permission notice shall be included in all 122 | copies or substantial portions of the Software. 123 | 124 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 125 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 126 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 127 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 128 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 129 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 130 | SOFTWARE. 131 | ``` 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-input-number", 3 | "version": "0.0.10", 4 | "description": "A configurable custom input number type for GraphQL with sanitization and validation.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "build-watch": "babel src --watch --out-dir lib", 9 | "clear": "rm -rf ./lib ./coverage ./.nyc_output", 10 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 11 | "nyc": "nyc npm test && nyc report --reporter=lcov", 12 | "prepublish": "npm run clear && npm test", 13 | "pretest": "npm run build", 14 | "start": "npm test", 15 | "test": "mocha", 16 | "test-watch": "mocha --watch", 17 | "update-D": "npm install --save-dev babel-cli@latest babel-preset-es2015@latest babel-preset-stage-0@latest babel-register@latest chai@latest chai-as-promised@latest coveralls@latest graphql@latest mocha@latest nyc@latest", 18 | "watch": "npm run build-watch & npm run test-watch" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/joonhocho/graphql-input-number.git" 23 | }, 24 | "keywords": [ 25 | "configurable", 26 | "custom", 27 | "input", 28 | "number", 29 | "scalar", 30 | "type", 31 | "GraphQL", 32 | "sanitization", 33 | "validation." 34 | ], 35 | "author": "Joon Ho Cho", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/joonhocho/graphql-input-number/issues" 39 | }, 40 | "homepage": "https://github.com/joonhocho/graphql-input-number#readme", 41 | "peerDependencies": { 42 | "graphql": "*" 43 | }, 44 | "dependencies": {}, 45 | "devDependencies": { 46 | "babel-cli": "^6.9.0", 47 | "babel-plugin-add-module-exports": "^0.2.1", 48 | "babel-preset-es2015": "^6.9.0", 49 | "babel-preset-stage-0": "^6.5.0", 50 | "babel-register": "^6.9.0", 51 | "chai": "^3.5.0", 52 | "chai-as-promised": "^5.3.0", 53 | "coveralls": "^2.11.9", 54 | "graphql": "*", 55 | "mocha": "^2.5.3", 56 | "nyc": "^6.4.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/float.js: -------------------------------------------------------------------------------- 1 | import {GraphQLScalarType} from 'graphql'; 2 | import {GraphQLError} from 'graphql/error'; 3 | import {Kind} from 'graphql/language'; 4 | 5 | 6 | // https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js 7 | 8 | function isSafeFloat(num) { 9 | return typeof num === 'number' && 10 | isFinite(num); 11 | } 12 | 13 | function coerceFloat(value) { 14 | if (value != null) { 15 | const num = Number(value); 16 | if (isSafeFloat(num)) { 17 | return num; 18 | } 19 | } 20 | return null; 21 | } 22 | 23 | export default ({ 24 | error, 25 | max, 26 | min, 27 | parse, 28 | sanitize, 29 | test, 30 | name, 31 | ...options, 32 | }) => { 33 | if (!name) { 34 | throw new Error('"name" is required'); 35 | } 36 | 37 | if (typeof error !== 'function') { 38 | error = ({value, ast, message}) => { 39 | const more = message ? ` ${message}.` : ''; 40 | throw new GraphQLError( 41 | `Invalid value ${JSON.stringify(value)}.${more}`, 42 | ast ? [ast] : [] 43 | ); 44 | }; 45 | } 46 | 47 | const parseValue = (value, ast) => { 48 | value = coerceFloat(value); 49 | if (value == null) { 50 | return null; 51 | } 52 | 53 | 54 | // Sanitization Phase 55 | 56 | if (sanitize) { 57 | value = sanitize(value); 58 | if (!isSafeFloat(value)) { 59 | return null; 60 | } 61 | } 62 | 63 | 64 | // Validation Phase 65 | 66 | if (min != null && value < min) { 67 | return error({ 68 | type: 'min', 69 | value, 70 | min, 71 | message: `Expected minimum "${min}"`, 72 | ast, 73 | }); 74 | } 75 | 76 | if (max != null && value > max) { 77 | return error({ 78 | type: 'max', 79 | value, 80 | max, 81 | message: `Expected maximum "${max}"`, 82 | ast, 83 | }); 84 | } 85 | 86 | if (test && !test(value)) { 87 | return error({ 88 | type: 'test', 89 | value, 90 | test, 91 | ast, 92 | }); 93 | } 94 | 95 | 96 | // Parse Phase 97 | 98 | if (parse) { 99 | return parse(value); 100 | } 101 | 102 | return value; 103 | }; 104 | 105 | return new GraphQLScalarType({ 106 | name, 107 | serialize: coerceFloat, 108 | parseValue, 109 | parseLiteral(ast) { 110 | const {kind, value} = ast; 111 | if (kind === Kind.FLOAT || kind === Kind.INT) { 112 | return parseValue(value, ast); 113 | } 114 | return null; 115 | }, 116 | ...options, 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import GraphQLInputInt from './int'; 2 | import GraphQLInputFloat from './float'; 3 | 4 | 5 | export { 6 | GraphQLInputInt, 7 | GraphQLInputInt as Int, 8 | GraphQLInputFloat, 9 | GraphQLInputFloat as Float, 10 | }; 11 | 12 | export default { 13 | GraphQLInputInt, 14 | Int: GraphQLInputInt, 15 | GraphQLInputFloat, 16 | Float: GraphQLInputFloat, 17 | }; 18 | -------------------------------------------------------------------------------- /src/int.js: -------------------------------------------------------------------------------- 1 | import {GraphQLScalarType} from 'graphql'; 2 | import {GraphQLError} from 'graphql/error'; 3 | import {Kind} from 'graphql/language'; 4 | 5 | 6 | // https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js 7 | const MAX_INT = 2147483647; 8 | const MIN_INT = -2147483648; 9 | 10 | function isSafeInteger(num) { 11 | return typeof num === 'number' && 12 | isFinite(num) && 13 | Math.floor(num) === num && 14 | num <= MAX_INT && 15 | num >= MIN_INT; 16 | } 17 | 18 | function coerceInt(value) { 19 | if (value != null) { 20 | const num = Number(value); 21 | if (isSafeInteger(num)) { 22 | return (num < 0 ? Math.ceil : Math.floor)(num); 23 | } 24 | } 25 | return null; 26 | } 27 | 28 | export default ({ 29 | error, 30 | max, 31 | min, 32 | parse, 33 | sanitize, 34 | test, 35 | name, 36 | ...options, 37 | }) => { 38 | if (!name) { 39 | throw new Error('"name" is required'); 40 | } 41 | 42 | if (typeof error !== 'function') { 43 | error = ({value, ast, message}) => { 44 | const more = message ? ` ${message}.` : ''; 45 | throw new GraphQLError( 46 | `Invalid value ${JSON.stringify(value)}.${more}`, 47 | ast ? [ast] : [] 48 | ); 49 | }; 50 | } 51 | 52 | const parseValue = (value, ast) => { 53 | value = coerceInt(value); 54 | if (value == null) { 55 | return null; 56 | } 57 | 58 | 59 | // Sanitization Phase 60 | 61 | if (sanitize) { 62 | value = sanitize(value); 63 | if (!isSafeInteger(value)) { 64 | return null; 65 | } 66 | } 67 | 68 | 69 | // Validation Phase 70 | 71 | if (min != null && value < min) { 72 | return error({ 73 | type: 'min', 74 | value, 75 | min, 76 | message: `Expected minimum "${min}"`, 77 | ast, 78 | }); 79 | } 80 | 81 | if (max != null && value > max) { 82 | return error({ 83 | type: 'max', 84 | value, 85 | max, 86 | message: `Expected maximum "${max}"`, 87 | ast, 88 | }); 89 | } 90 | 91 | if (test && !test(value)) { 92 | return error({ 93 | type: 'test', 94 | value, 95 | test, 96 | ast, 97 | }); 98 | } 99 | 100 | 101 | // Parse Phase 102 | 103 | if (parse) { 104 | return parse(value); 105 | } 106 | 107 | return value; 108 | }; 109 | 110 | return new GraphQLScalarType({ 111 | name, 112 | serialize: coerceInt, 113 | parseValue, 114 | parseLiteral(ast) { 115 | const {kind, value} = ast; 116 | if (kind === Kind.INT) { 117 | return parseValue(value, ast); 118 | } 119 | return null; 120 | }, 121 | ...options, 122 | }); 123 | }; 124 | -------------------------------------------------------------------------------- /test/float.js: -------------------------------------------------------------------------------- 1 | import { 2 | graphql, 3 | GraphQLSchema, 4 | GraphQLFloat, 5 | GraphQLObjectType, 6 | } from 'graphql'; 7 | import {describe, it} from 'mocha'; 8 | import {expect} from 'chai'; 9 | import {GraphQLInputFloat} from '../lib'; 10 | 11 | 12 | const getSchema = (options) => new GraphQLSchema({ 13 | query: new GraphQLObjectType({ 14 | name: 'Query', 15 | fields: { 16 | input: { 17 | type: GraphQLFloat, 18 | args: { 19 | value: { 20 | type: GraphQLInputFloat(options), 21 | }, 22 | }, 23 | resolve: (_, {value}) => value, 24 | }, 25 | }, 26 | }), 27 | }); 28 | 29 | 30 | const runQuery = (schema, value) => 31 | graphql(schema, `{ input(value: ${JSON.stringify(value)}) }`) 32 | .then((res) => res.data.input); 33 | 34 | const testEqual = (schema, done, value, expected) => 35 | runQuery(schema, value) 36 | .then((input) => { expect(input).to.eql(expected); }) 37 | .then(done, done); 38 | 39 | const testError = (schema, done, value, expected) => 40 | graphql(schema, `{ input(value: ${JSON.stringify(value)}) }`) 41 | .then((res) => { 42 | expect(res.errors[0].message).to.match(expected); 43 | }) 44 | .then(done, done); 45 | 46 | 47 | describe('GraphQLInputFloat', () => { 48 | it('default', (done) => { 49 | const schema = getSchema({ 50 | name: 'default', 51 | }); 52 | 53 | const value = 3.1; 54 | const expected = value; 55 | 56 | testEqual(schema, done, value, expected); 57 | }); 58 | 59 | it('sanitize', (done) => { 60 | const schema = getSchema({ 61 | name: 'sanitize', 62 | sanitize: (x) => 2 * x, 63 | }); 64 | 65 | const value = 3.1; 66 | const expected = 6.2; 67 | 68 | testEqual(schema, done, value, expected); 69 | }); 70 | 71 | it('non-float bad', (done) => { 72 | const schema = getSchema({ 73 | name: 'NonFloat', 74 | }); 75 | 76 | const value = '3.1'; 77 | 78 | testError(schema, done, value, /type/i); 79 | }); 80 | 81 | it('non-float ok', (done) => { 82 | const schema = getSchema({ 83 | name: 'NonFloat', 84 | }); 85 | 86 | const value = 3.1; 87 | 88 | testEqual(schema, done, value, value); 89 | }); 90 | 91 | it('min bad', (done) => { 92 | const schema = getSchema({ 93 | name: 'min', 94 | min: 3, 95 | }); 96 | 97 | const value = 2.9; 98 | 99 | testError(schema, done, value, /minimum.*3/i); 100 | }); 101 | 102 | it('min ok', (done) => { 103 | const schema = getSchema({ 104 | name: 'min', 105 | min: 3, 106 | }); 107 | 108 | const value = 3.1; 109 | 110 | testEqual(schema, done, value, value); 111 | }); 112 | 113 | it('max bad', (done) => { 114 | const schema = getSchema({ 115 | name: 'max', 116 | max: 5, 117 | }); 118 | 119 | const value = 5.1; 120 | 121 | testError(schema, done, value, /maximum.*5/i); 122 | }); 123 | 124 | it('max ok', (done) => { 125 | const schema = getSchema({ 126 | name: 'max', 127 | max: 5, 128 | }); 129 | 130 | const value = 4.9; 131 | 132 | testEqual(schema, done, value, value); 133 | }); 134 | 135 | it('test bad', (done) => { 136 | const schema = getSchema({ 137 | name: 'test', 138 | test: (x) => x < 3, 139 | }); 140 | 141 | const value = 3.1; 142 | 143 | testError(schema, done, value, /invalid/i); 144 | }); 145 | 146 | it('test ok', (done) => { 147 | const schema = getSchema({ 148 | name: 'test', 149 | test: (x) => x < 3, 150 | }); 151 | 152 | const value = 2.9; 153 | 154 | testEqual(schema, done, value, value); 155 | }); 156 | 157 | it('error', (done) => { 158 | const schema = getSchema({ 159 | name: 'error', 160 | min: 3, 161 | error: (err) => err.value - 3, 162 | }); 163 | 164 | const value = 2; 165 | 166 | runQuery(schema, value) 167 | .then((input) => { 168 | expect(input).to.equal(-1); 169 | }) 170 | .then(done, done); 171 | }); 172 | 173 | it('parse', (done) => { 174 | const schema = getSchema({ 175 | name: 'parse', 176 | max: 5, 177 | parse: (x) => 2 * x, 178 | }); 179 | 180 | const value = 3.1; 181 | const expected = 6.2; 182 | 183 | testEqual(schema, done, value, expected); 184 | }); 185 | 186 | it('name', () => { 187 | expect(() => GraphQLInputFloat({ 188 | // name is required 189 | })).to.throw(/name/i); 190 | }); 191 | 192 | it('description', () => { 193 | const description = 'this is description'; 194 | const type = GraphQLInputFloat({ 195 | name: 'desc', 196 | description, 197 | }); 198 | expect(type.description).to.equal(description); 199 | }); 200 | 201 | it('serialize', (done) => { 202 | const schema = new GraphQLSchema({ 203 | query: new GraphQLObjectType({ 204 | name: 'Query', 205 | fields: { 206 | output: { 207 | type: GraphQLInputFloat({ 208 | name: 'output', 209 | parse: (x) => 2 * x, 210 | }), 211 | resolve: () => 3.1, 212 | }, 213 | }, 214 | }), 215 | }); 216 | 217 | graphql(schema, '{ output }') 218 | .then((res) => { 219 | // parse is only applied to input 220 | expect(res.data.output).to.equal(3.1); 221 | }) 222 | .then(done, done); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /test/int.js: -------------------------------------------------------------------------------- 1 | import { 2 | graphql, 3 | GraphQLSchema, 4 | GraphQLInt, 5 | GraphQLObjectType, 6 | } from 'graphql'; 7 | import {describe, it} from 'mocha'; 8 | import {expect} from 'chai'; 9 | import {GraphQLInputInt} from '../lib'; 10 | 11 | 12 | const getSchema = (options) => new GraphQLSchema({ 13 | query: new GraphQLObjectType({ 14 | name: 'Query', 15 | fields: { 16 | input: { 17 | type: GraphQLInt, 18 | args: { 19 | value: { 20 | type: GraphQLInputInt(options), 21 | }, 22 | }, 23 | resolve: (_, {value}) => value, 24 | }, 25 | }, 26 | }), 27 | }); 28 | 29 | 30 | const runQuery = (schema, value) => 31 | graphql(schema, `{ input(value: ${JSON.stringify(value)}) }`) 32 | .then((res) => res.data.input); 33 | 34 | const testEqual = (schema, done, value, expected) => 35 | runQuery(schema, value) 36 | .then((input) => { expect(input).to.eql(expected); }) 37 | .then(done, done); 38 | 39 | const testError = (schema, done, value, expected) => 40 | graphql(schema, `{ input(value: ${JSON.stringify(value)}) }`) 41 | .then((res) => { 42 | expect(res.errors[0].message).to.match(expected); 43 | }) 44 | .then(done, done); 45 | 46 | 47 | describe('GraphQLInputInt', () => { 48 | it('default', (done) => { 49 | const schema = getSchema({ 50 | name: 'default', 51 | }); 52 | 53 | const value = 3; 54 | const expected = value; 55 | 56 | testEqual(schema, done, value, expected); 57 | }); 58 | 59 | it('sanitize', (done) => { 60 | const schema = getSchema({ 61 | name: 'sanitize', 62 | sanitize: (x) => 2 * x, 63 | }); 64 | 65 | const value = 3; 66 | const expected = 6; 67 | 68 | testEqual(schema, done, value, expected); 69 | }); 70 | 71 | it('non-int bad', (done) => { 72 | const schema = getSchema({ 73 | name: 'NonInt', 74 | }); 75 | 76 | const value = '3'; 77 | 78 | testError(schema, done, value, /type/i); 79 | }); 80 | 81 | it('non-int ok', (done) => { 82 | const schema = getSchema({ 83 | name: 'NonInt', 84 | }); 85 | 86 | const value = 3; 87 | 88 | testEqual(schema, done, value, value); 89 | }); 90 | 91 | it('min bad', (done) => { 92 | const schema = getSchema({ 93 | name: 'min', 94 | min: 3, 95 | }); 96 | 97 | const value = 2; 98 | 99 | testError(schema, done, value, /minimum.*3/i); 100 | }); 101 | 102 | it('min ok', (done) => { 103 | const schema = getSchema({ 104 | name: 'min', 105 | min: 3, 106 | }); 107 | 108 | const value = 3; 109 | 110 | testEqual(schema, done, value, value); 111 | }); 112 | 113 | it('max bad', (done) => { 114 | const schema = getSchema({ 115 | name: 'max', 116 | max: 5, 117 | }); 118 | 119 | const value = 6; 120 | 121 | testError(schema, done, value, /maximum.*5/i); 122 | }); 123 | 124 | it('max ok', (done) => { 125 | const schema = getSchema({ 126 | name: 'max', 127 | max: 5, 128 | }); 129 | 130 | const value = 5; 131 | 132 | testEqual(schema, done, value, value); 133 | }); 134 | 135 | it('test bad', (done) => { 136 | const schema = getSchema({ 137 | name: 'test', 138 | test: (x) => x < 3, 139 | }); 140 | 141 | const value = 3; 142 | 143 | testError(schema, done, value, /invalid/i); 144 | }); 145 | 146 | it('test ok', (done) => { 147 | const schema = getSchema({ 148 | name: 'test', 149 | test: (x) => x < 3, 150 | }); 151 | 152 | const value = 2; 153 | 154 | testEqual(schema, done, value, value); 155 | }); 156 | 157 | it('error', (done) => { 158 | const schema = getSchema({ 159 | name: 'error', 160 | min: 3, 161 | error: (err) => err.value - 3, 162 | }); 163 | 164 | const value = 2; 165 | 166 | runQuery(schema, value) 167 | .then((input) => { 168 | expect(input).to.equal(-1); 169 | }) 170 | .then(done, done); 171 | }); 172 | 173 | it('parse', (done) => { 174 | const schema = getSchema({ 175 | name: 'parse', 176 | max: 5, 177 | parse: (x) => 2 * x, 178 | }); 179 | 180 | const value = 3; 181 | const expected = 6; 182 | 183 | testEqual(schema, done, value, expected); 184 | }); 185 | 186 | it('name', () => { 187 | expect(() => GraphQLInputInt({ 188 | // name is required 189 | })).to.throw(/name/i); 190 | }); 191 | 192 | it('description', () => { 193 | const description = 'this is description'; 194 | const type = GraphQLInputInt({ 195 | name: 'desc', 196 | description, 197 | }); 198 | expect(type.description).to.equal(description); 199 | }); 200 | 201 | it('serialize', (done) => { 202 | const schema = new GraphQLSchema({ 203 | query: new GraphQLObjectType({ 204 | name: 'Query', 205 | fields: { 206 | output: { 207 | type: GraphQLInputInt({ 208 | name: 'output', 209 | parse: (x) => 2 * x, 210 | }), 211 | resolve: () => 3, 212 | }, 213 | }, 214 | }), 215 | }); 216 | 217 | graphql(schema, '{ output }') 218 | .then((res) => { 219 | // parse is only applied to input 220 | expect(res.data.output).to.equal(3); 221 | }) 222 | .then(done, done); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --require ./test/setup.js 3 | --reporter spec 4 | --timeout 5000 5 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | chai.use(require('chai-as-promised')); 3 | --------------------------------------------------------------------------------