├── .ackrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .yarnrc ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── common.test.ts ├── common.ts ├── float.test.ts ├── float.ts ├── index.ts ├── int.test.ts ├── int.ts ├── string.test.ts ├── string.ts └── ts.ts ├── tsconfig.browser.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.module.json ├── tsconfig.node.json ├── tslint.json └── yarn.lock /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=.git 2 | --ignore-dir=.gradle 3 | --ignore-dir=.idea 4 | --ignore-dir=.rpt2_cache 5 | --ignore-dir=__generated__ 6 | --ignore-dir=build 7 | --ignore-dir=cjs 8 | --ignore-dir=dist 9 | --ignore-dir=es5 10 | --ignore-dir=es6 11 | --ignore-dir=lib 12 | --ignore-dir=node_modules 13 | --ignore-file=match:.DS_Store 14 | --ignore-file=match:.ackrc 15 | --ignore-file=match:.babelrc 16 | --ignore-file=match:.buckconfig 17 | --ignore-file=match:.eslintignore 18 | --ignore-file=match:.eslintrc 19 | --ignore-file=match:.flowconfig 20 | --ignore-file=match:.gitattributes 21 | --ignore-file=match:.gitignore 22 | --ignore-file=match:.iml 23 | --ignore-file=match:.npmignore 24 | --ignore-file=match:.watchmanconfig 25 | --ignore-file=match:Podfile.lock 26 | --ignore-file=match:npm-debug.log 27 | --ignore-file=match:package-lock.json 28 | --ignore-file=match:yarn.lock 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vue,vim,git,node,code,nuxt,xcode,vuejs,linux,macos,kotlin,windows,android,angular,flutter,webstorm,firebase,reactnative,objective-c,androidstudio 2 | !**/ios/**/default.mode1v3 3 | !**/ios/**/default.mode2v3 4 | !**/ios/**/default.pbxuser 5 | !**/ios/**/default.perspectivev3 6 | !*.xcodeproj/project.pbxproj 7 | !*.xcodeproj/xcshareddata/ 8 | !*.xcworkspace/contents.xcworkspacedata 9 | !.vscode/extensions.json 10 | !.vscode/launch.json 11 | !.vscode/settings.json 12 | !.vscode/tasks.json 13 | !/gradle/wrapper/gradle-wrapper.jar 14 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 15 | !default.mode1v3 16 | !default.mode2v3 17 | !default.pbxuser 18 | !default.perspectivev3 19 | !gradle-wrapper.jar 20 | $RECYCLE.BIN/ 21 | **/android/**/GeneratedPluginRegistrant.java 22 | **/android/**/gradle-wrapper.jar 23 | **/android/.gradle 24 | **/android/captures/ 25 | **/android/gradlew 26 | **/android/gradlew.bat 27 | **/android/local.properties 28 | **/doc/api/ 29 | **/ios/**/*.mode1v3 30 | **/ios/**/*.mode2v3 31 | **/ios/**/*.moved-aside 32 | **/ios/**/*.pbxuser 33 | **/ios/**/*.perspectivev3 34 | **/ios/**/*sync/ 35 | **/ios/**/.sconsign.dblite 36 | **/ios/**/.symlinks/ 37 | **/ios/**/.tags* 38 | **/ios/**/.vagrant/ 39 | **/ios/**/DerivedData/ 40 | **/ios/**/Icon? 41 | **/ios/**/Pods/ 42 | **/ios/**/profile 43 | **/ios/**/xcuserdata 44 | **/ios/.generated/ 45 | **/ios/Flutter/App.framework 46 | **/ios/Flutter/Flutter.framework 47 | **/ios/Flutter/Generated.xcconfig 48 | **/ios/Flutter/app.flx 49 | **/ios/Flutter/app.zip 50 | **/ios/Flutter/flutter_assets/ 51 | **/ios/Runner/GeneratedPluginRegistrant.* 52 | **/ios/ServiceDefinitions.json 53 | **/xcshareddata/WorkspaceSettings.xcsettings 54 | *-debug.log* 55 | *-error.log* 56 | *.BACKUP.* 57 | *.BASE.* 58 | *.LOCAL.* 59 | *.REMOTE.* 60 | *.aab 61 | *.ap_ 62 | *.apk 63 | *.class 64 | *.ctxt 65 | *.dSYM 66 | *.dSYM.zip 67 | *.dex 68 | *.ear 69 | *.hmap 70 | *.iml 71 | *.ipa 72 | *.ipr 73 | *.iws 74 | *.jar 75 | *.jks 76 | *.keystore 77 | *.lcov 78 | *.lnk 79 | *.log 80 | *.mode1v3 81 | *.mode2v3 82 | *.moved-aside 83 | *.nar 84 | *.orig 85 | *.pbxuser 86 | *.perspectivev3 87 | *.pid 88 | *.pid.lock 89 | *.rar 90 | *.rs.bk 91 | *.seed 92 | *.stackdump 93 | *.swp 94 | *.tar.gz 95 | *.tgz 96 | *.tsbuildinfo 97 | *.war 98 | *.xccheckout 99 | *.xcodeproj/* 100 | *.xcscmblueprint 101 | *_BACKUP_*.txt 102 | *_BASE_*.txt 103 | *_LOCAL_*.txt 104 | *_REMOTE_*.txt 105 | *google-services.json 106 | *secrets* 107 | *~ 108 | .AppleDB 109 | .AppleDesktop 110 | .AppleDouble 111 | .DS_Store* 112 | .DocumentRevisions-V100 113 | .LSOverride 114 | .Spotlight-V100 115 | .TemporaryItems 116 | .Trash-* 117 | .Trashes 118 | .VolumeIcon.icns 119 | ._* 120 | .apdisk 121 | .buckconfig.local 122 | .buckd/ 123 | .buckversion 124 | .cache 125 | .cargo-ok 126 | .classpath 127 | .com.apple.timemachine.donotpresent 128 | .cproject 129 | .dart_tool/ 130 | .directory 131 | .dynamodb/ 132 | .env 133 | .env.* 134 | .eslintcache 135 | .expo 136 | .externalNativeBuild 137 | .fakebuckversion 138 | .firebase 139 | .firebaserc 140 | .flutter-plugins 141 | .flutter-plugins 142 | .fseventsd 143 | .fuse_hidden* 144 | .fusebox/ 145 | .gradle/ 146 | .gradletasknamecache 147 | .grunt 148 | .idea 149 | .idea_modules/ 150 | .lock-wscript 151 | .mtj.tmp/ 152 | .navigation/ 153 | .netrwhist 154 | .next 155 | .nfs* 156 | .node_repl_history 157 | .npm 158 | .npmrc 159 | .nuxt 160 | .nyc_output 161 | .packages 162 | .project 163 | .pub-cache/ 164 | .pub/ 165 | .rpt2_cache 166 | .runtimeconfig.json 167 | .sass-cache 168 | .serverless/ 169 | .settings/ 170 | .signing/ 171 | .tern-port 172 | .vscode/* 173 | .vuepress/dist 174 | .yarn-integrity 175 | /*.gcno 176 | Cargo.lock 177 | Carthage/Build 178 | DerivedData/ 179 | Icon 180 | Network Trash Folder 181 | Release/ 182 | Session.vim 183 | Sessionx.vim 184 | Temporary Items 185 | Thumbs.db 186 | Thumbs.db:encryptable 187 | [._]*.s[a-v][a-z] 188 | [._]*.sw[a-p] 189 | [._]*.un~ 190 | [._]s[a-rt-v][a-z] 191 | [._]ss[a-gi-z] 192 | [._]sw[a-p] 193 | [Dd]esktop.ini 194 | __generated__ 195 | atlassian-ide-plugin.xml 196 | bin/ 197 | bower_components 198 | buck-out/ 199 | build/ 200 | captures/ 201 | cmake-build-*/ 202 | com_crashlytics_export_strings.xml 203 | connect.lock 204 | coverage/ 205 | crashlytics-build.properties 206 | crashlytics.properties 207 | dist/ 208 | docs/_book 209 | ehthumbs.db 210 | ehthumbs_vista.db 211 | fabric.properties 212 | fastlane/Preview.html 213 | fastlane/readme.md 214 | fastlane/report.xml 215 | fastlane/screenshots 216 | fastlane/screenshots/**/*.png 217 | fastlane/test_output 218 | freeline.py 219 | freeline/ 220 | freeline_project_description.json 221 | gen-external-apklibs 222 | gen/ 223 | gradle-app.setting 224 | hs_err_pid* 225 | iOSInjectionProject/ 226 | jspm_packages/ 227 | lib-cov 228 | lint/generated/ 229 | lint/intermediates/ 230 | lint/outputs/ 231 | lint/tmp/ 232 | local.properties 233 | logs 234 | node_modules 235 | now-secrets.json 236 | out/ 237 | output.json 238 | pids 239 | proguard/ 240 | release/ 241 | report.*.json 242 | tmp/ 243 | vcs.xml 244 | xcuserdata/ 245 | # End 246 | 247 | lib 248 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vue,vim,git,node,code,nuxt,xcode,vuejs,linux,macos,kotlin,windows,android,angular,flutter,webstorm,firebase,reactnative,objective-c,androidstudio 2 | !**/ios/**/default.mode1v3 3 | !**/ios/**/default.mode2v3 4 | !**/ios/**/default.pbxuser 5 | !**/ios/**/default.perspectivev3 6 | !*.xcodeproj/project.pbxproj 7 | !*.xcodeproj/xcshareddata/ 8 | !*.xcworkspace/contents.xcworkspacedata 9 | !.vscode/extensions.json 10 | !.vscode/launch.json 11 | !.vscode/settings.json 12 | !.vscode/tasks.json 13 | !/gradle/wrapper/gradle-wrapper.jar 14 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 15 | !default.mode1v3 16 | !default.mode2v3 17 | !default.pbxuser 18 | !default.perspectivev3 19 | !gradle-wrapper.jar 20 | $RECYCLE.BIN/ 21 | **/android/**/GeneratedPluginRegistrant.java 22 | **/android/**/gradle-wrapper.jar 23 | **/android/.gradle 24 | **/android/captures/ 25 | **/android/gradlew 26 | **/android/gradlew.bat 27 | **/android/local.properties 28 | **/doc/api/ 29 | **/ios/**/*.mode1v3 30 | **/ios/**/*.mode2v3 31 | **/ios/**/*.moved-aside 32 | **/ios/**/*.pbxuser 33 | **/ios/**/*.perspectivev3 34 | **/ios/**/*sync/ 35 | **/ios/**/.sconsign.dblite 36 | **/ios/**/.symlinks/ 37 | **/ios/**/.tags* 38 | **/ios/**/.vagrant/ 39 | **/ios/**/DerivedData/ 40 | **/ios/**/Icon? 41 | **/ios/**/Pods/ 42 | **/ios/**/profile 43 | **/ios/**/xcuserdata 44 | **/ios/.generated/ 45 | **/ios/Flutter/App.framework 46 | **/ios/Flutter/Flutter.framework 47 | **/ios/Flutter/Generated.xcconfig 48 | **/ios/Flutter/app.flx 49 | **/ios/Flutter/app.zip 50 | **/ios/Flutter/flutter_assets/ 51 | **/ios/Runner/GeneratedPluginRegistrant.* 52 | **/ios/ServiceDefinitions.json 53 | **/xcshareddata/WorkspaceSettings.xcsettings 54 | *-debug.log* 55 | *-error.log* 56 | *.BACKUP.* 57 | *.BASE.* 58 | *.LOCAL.* 59 | *.REMOTE.* 60 | *.aab 61 | *.ap_ 62 | *.apk 63 | *.class 64 | *.ctxt 65 | *.dSYM 66 | *.dSYM.zip 67 | *.dex 68 | *.ear 69 | *.hmap 70 | *.iml 71 | *.ipa 72 | *.ipr 73 | *.iws 74 | *.jar 75 | *.jks 76 | *.keystore 77 | *.lcov 78 | *.lnk 79 | *.log 80 | *.mode1v3 81 | *.mode2v3 82 | *.moved-aside 83 | *.nar 84 | *.orig 85 | *.pbxuser 86 | *.perspectivev3 87 | *.pid 88 | *.pid.lock 89 | *.rar 90 | *.rs.bk 91 | *.seed 92 | *.stackdump 93 | *.swp 94 | *.tar.gz 95 | *.tgz 96 | *.tsbuildinfo 97 | *.war 98 | *.xccheckout 99 | *.xcodeproj/* 100 | *.xcscmblueprint 101 | *_BACKUP_*.txt 102 | *_BASE_*.txt 103 | *_LOCAL_*.txt 104 | *_REMOTE_*.txt 105 | *google-services.json 106 | *secrets* 107 | *~ 108 | .AppleDB 109 | .AppleDesktop 110 | .AppleDouble 111 | .DS_Store* 112 | .DocumentRevisions-V100 113 | .LSOverride 114 | .Spotlight-V100 115 | .TemporaryItems 116 | .Trash-* 117 | .Trashes 118 | .VolumeIcon.icns 119 | ._* 120 | .apdisk 121 | .buckconfig.local 122 | .buckd/ 123 | .buckversion 124 | .cache 125 | .cargo-ok 126 | .classpath 127 | .com.apple.timemachine.donotpresent 128 | .cproject 129 | .dart_tool/ 130 | .directory 131 | .dynamodb/ 132 | .env 133 | .env.* 134 | .eslintcache 135 | .expo 136 | .externalNativeBuild 137 | .fakebuckversion 138 | .firebase 139 | .firebaserc 140 | .flutter-plugins 141 | .flutter-plugins 142 | .fseventsd 143 | .fuse_hidden* 144 | .fusebox/ 145 | .gradle/ 146 | .gradletasknamecache 147 | .grunt 148 | .idea 149 | .idea_modules/ 150 | .lock-wscript 151 | .mtj.tmp/ 152 | .navigation/ 153 | .netrwhist 154 | .next 155 | .nfs* 156 | .node_repl_history 157 | .npm 158 | .npmrc 159 | .nuxt 160 | .nyc_output 161 | .packages 162 | .project 163 | .pub-cache/ 164 | .pub/ 165 | .rpt2_cache 166 | .runtimeconfig.json 167 | .sass-cache 168 | .serverless/ 169 | .settings/ 170 | .signing/ 171 | .tern-port 172 | .vscode/* 173 | .vuepress/dist 174 | .yarn-integrity 175 | /*.gcno 176 | Cargo.lock 177 | Carthage/Build 178 | DerivedData/ 179 | Icon 180 | Network Trash Folder 181 | Release/ 182 | Session.vim 183 | Sessionx.vim 184 | Temporary Items 185 | Thumbs.db 186 | Thumbs.db:encryptable 187 | [._]*.s[a-v][a-z] 188 | [._]*.sw[a-p] 189 | [._]*.un~ 190 | [._]s[a-rt-v][a-z] 191 | [._]ss[a-gi-z] 192 | [._]sw[a-p] 193 | [Dd]esktop.ini 194 | __generated__ 195 | atlassian-ide-plugin.xml 196 | bin/ 197 | bower_components 198 | buck-out/ 199 | build/ 200 | captures/ 201 | cmake-build-*/ 202 | com_crashlytics_export_strings.xml 203 | connect.lock 204 | coverage/ 205 | crashlytics-build.properties 206 | crashlytics.properties 207 | dist/ 208 | docs/_book 209 | ehthumbs.db 210 | ehthumbs_vista.db 211 | fabric.properties 212 | fastlane/Preview.html 213 | fastlane/readme.md 214 | fastlane/report.xml 215 | fastlane/screenshots 216 | fastlane/screenshots/**/*.png 217 | fastlane/test_output 218 | freeline.py 219 | freeline/ 220 | freeline_project_description.json 221 | gen-external-apklibs 222 | gen/ 223 | gradle-app.setting 224 | hs_err_pid* 225 | iOSInjectionProject/ 226 | jspm_packages/ 227 | lib-cov 228 | lint/generated/ 229 | lint/intermediates/ 230 | lint/outputs/ 231 | lint/tmp/ 232 | local.properties 233 | logs 234 | node_modules 235 | now-secrets.json 236 | out/ 237 | output.json 238 | pids 239 | proguard/ 240 | release/ 241 | report.*.json 242 | tmp/ 243 | vcs.xml 244 | xcuserdata/ 245 | # End 246 | 247 | # Additional to .gitignore 248 | !dist/ 249 | !out/ 250 | *.config.js 251 | *.config.ts 252 | *.md 253 | .*ignore 254 | .*rc 255 | .gitignore 256 | .travis.yml 257 | now*.json 258 | src 259 | tsconfig*.json 260 | tslint.json 261 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | - "lts/*" 6 | - "12" 7 | - "11" 8 | - "10" 9 | - "9" 10 | - "8" 11 | 12 | after_success: npm run test:coverage:report 13 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | "--install.ignore-engines" true 2 | ignore-engines true 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | # graphql-scalar 2 | Configurable custom GraphQL Scalars (string, number, date, etc) with sanitization / validation / transformation in TypeScript. 3 | 4 | [![npm version](https://badge.fury.io/js/graphql-scalar.svg)](https://badge.fury.io/js/graphql-scalar) 5 | [![Build Status](https://travis-ci.org/joonhocho/graphql-scalar.svg?branch=master)](https://travis-ci.org/joonhocho/graphql-scalar) 6 | [![Coverage Status](https://coveralls.io/repos/github/joonhocho/graphql-scalar/badge.svg?branch=master)](https://coveralls.io/github/joonhocho/graphql-scalar?branch=master) 7 | [![Dependency Status](https://david-dm.org/joonhocho/graphql-scalar.svg)](https://david-dm.org/joonhocho/graphql-scalar) 8 | ![npm type definitions](https://img.shields.io/npm/types/graphql-scalar.svg) 9 | [![GitHub](https://img.shields.io/github/license/joonhocho/graphql-scalar.svg)](https://github.com/joonhocho/graphql-scalar/blob/master/LICENSE) 10 | 11 | TypeScript version (with breaking changes) of the following repos: 12 | [joonhocho/graphql-input-number](https://github.com/joonhocho/graphql-input-number) 13 | [joonhocho/graphql-input-string](https://github.com/joonhocho/graphql-input-string) 14 | 15 | ## Get Started 16 | ``` 17 | npm install graphql-scalar 18 | ``` 19 | or 20 | ``` 21 | yarn add graphql-scalar 22 | ``` 23 | 24 | ## How to Use 25 | ```typescript 26 | import { createStringScalar, createIntScalar, createFloatScalar } from 'graphql-scalar'; 27 | 28 | const stringScalar = createStringScalar({ 29 | name: string; 30 | description?: string; 31 | capitalize?: 'characters' | 'words' | 'sentences' | 'first'; 32 | collapseWhitespace?: boolean; 33 | lowercase?: boolean; 34 | maxLength?: number; 35 | minLength?: number; 36 | nonEmpty?: boolean; 37 | pattern?: RegExp | string; 38 | singleline?: string; 39 | trim?: boolean; 40 | trimLeft?: boolean; 41 | trimRight?: boolean; 42 | truncate?: number; 43 | uppercase?: boolean; 44 | coerce?: ScalarCoerceFunction; 45 | errorHandler?: ScalarParseErrorHandler; 46 | parse?: ScalarParseFunction; 47 | sanitize?: ScalarSanitizeFunction; 48 | serialize?: ScalarSerializeFunction; 49 | validate?: ScalarValidateFunction; 50 | }) 51 | 52 | const intScalar = createIntScalar({ 53 | name: string; 54 | description?: string; 55 | maximum?: number; 56 | minimum?: number; 57 | coerce?: ScalarCoerceFunction; 58 | errorHandler?: ScalarParseErrorHandler; 59 | parse?: ScalarParseFunction; 60 | sanitize?: ScalarSanitizeFunction; 61 | serialize?: ScalarSerializeFunction; 62 | validate?: ScalarValidateFunction; 63 | }) 64 | ``` 65 | 66 | ## License 67 | [MIT License](https://github.com/joonhocho/graphql-scalar/blob/master/LICENSE) 68 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | tsConfig: 'tsconfig.json', 5 | }, 6 | }, 7 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'], 8 | moduleNameMapper: { 9 | '^_src/(.*)': '/src/$1', 10 | }, 11 | testEnvironment: 'node', 12 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(t|j)sx?$', 13 | transform: { 14 | '^.+\\.tsx?$': 'ts-jest', 15 | }, 16 | transformIgnorePatterns: [], 17 | preset: 'ts-jest', 18 | testMatch: null, 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-scalar", 3 | "version": "0.1.0", 4 | "description": "Configurable custom GraphQL Scalars (string, number, date, etc) with sanitization / validation / transformation in TypeScript.", 5 | "keywords": [ 6 | "configurable", 7 | "custom", 8 | "GraphQL", 9 | "scalars", 10 | "GraphQLScalarType", 11 | "string", 12 | "number", 13 | "date", 14 | "timestamp", 15 | "sanitization", 16 | "validation", 17 | "transformation", 18 | "TypeScript", 19 | "graphql-input-number", 20 | "graphql-input-string" 21 | ], 22 | "author": "Joon Ho Cho", 23 | "license": "MIT", 24 | "homepage": "https://github.com/joonhocho/graphql-scalar#readme", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/joonhocho/graphql-scalar.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/joonhocho/graphql-scalar/issues" 31 | }, 32 | "module": "lib/index.js", 33 | "main": "dist/node/index.js", 34 | "browser": "dist/browser/index.js", 35 | "types": "lib/index.d.ts", 36 | "sideEffects": false, 37 | "scripts": { 38 | "all": "npm run clean && npm run format && npm run lint:fix && npm run build:all && npm run test", 39 | "build:all": "npm run build:module && npm run build:node && npm run build:browser", 40 | "build:browser": "tsc -p ./tsconfig.browser.json && tscpaths -p ./tsconfig.browser.json -s ./src -o ./dist/browser", 41 | "build:module": "tsc -p ./tsconfig.module.json && tscpaths -p ./tsconfig.module.json -s ./src -o ./lib", 42 | "build:node": "tsc -p ./tsconfig.node.json && tscpaths -p ./tsconfig.node.json -s ./src -o ./dist/node", 43 | "clean": "shx rm -rf ./lib ./dist ./coverage", 44 | "format": "prettier --write \"./*.{js,jsx,ts,tsx}\" \"./src/**/*.{js,jsx,ts,tsx}\"", 45 | "lint": "tslint -c ./tslint.json \"src/**/*.ts\"", 46 | "lint:fix": "tslint --fix -c ./tslint.json \"src/**/*.ts\"", 47 | "precommit": "npm run all", 48 | "prepublishOnly": "npm run all", 49 | "reinstall": "shx rm -rf ./node_modules ./package-lock.json ./yarn.lock && yarn", 50 | "start": "npm run test", 51 | "test": "jest", 52 | "test:coverage": "jest --coverage", 53 | "test:coverage:report": "jest --coverage && cat ./coverage/lcov.info | coveralls", 54 | "test:watch": "jest --watch" 55 | }, 56 | "pre-commit": "precommit", 57 | "dependencies": { 58 | "tsdef": "^0.0.14" 59 | }, 60 | "peerDependencies": { 61 | "graphql": "*", 62 | "tslib": "^1.10.0" 63 | }, 64 | "devDependencies": { 65 | "@types/graphql": "^14.5.0", 66 | "@types/jest": "^29.5.0", 67 | "@types/node": "^18.15.5", 68 | "coveralls": "^3.1.1", 69 | "graphql": "^14.*.*", 70 | "jest": "^29.5.0", 71 | "pre-commit": "^1.2.2", 72 | "prettier": "^2.8.6", 73 | "shx": "^0.3.4", 74 | "ts-jest": "^29.0.5", 75 | "tscpaths": "^0.0.9", 76 | "tslint": "^5.20.1", 77 | "tslint-config-airbnb": "^5.11.2", 78 | "tslint-config-prettier": "^1.18.0", 79 | "typescript": "^5.0.2" 80 | }, 81 | "engines": { 82 | "node": ">=8.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | jsxBracketSameLine: false, 5 | singleQuote: true, 6 | trailingComma: 'es5', 7 | }; 8 | -------------------------------------------------------------------------------- /src/common.test.ts: -------------------------------------------------------------------------------- 1 | import { graphql, GraphQLSchema } from 'graphql'; 2 | 3 | export const runQuery = async ( 4 | schema: GraphQLSchema, 5 | value: any 6 | ): Promise => { 7 | const res = await graphql( 8 | schema, 9 | `{ input(value: ${JSON.stringify(value)}) }` 10 | ); 11 | const { data, errors } = res; 12 | if (errors) { 13 | throw new Error(errors[0].message); 14 | } 15 | return data!.input; 16 | }; 17 | 18 | export const assertEqual = async ( 19 | schema: GraphQLSchema, 20 | value: any, 21 | expected: any 22 | ): Promise => { 23 | const actual = await runQuery(schema, value); 24 | return expect(actual).toEqual(expected); 25 | }; 26 | 27 | export const assertThrow = async ( 28 | schema: GraphQLSchema, 29 | value: any, 30 | pattern: any 31 | ): Promise => { 32 | await expect(runQuery(schema, value)).rejects.toThrowError(pattern); 33 | }; 34 | 35 | test('', () => { 36 | expect(1).toBe(1); 37 | }); 38 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanValueNode, 3 | EnumValueNode, 4 | FloatValueNode, 5 | IntValueNode, 6 | Kind, 7 | StringValueNode, 8 | ValueNode, 9 | } from 'graphql'; 10 | import { GraphQLError } from 'graphql/error'; 11 | import { ScalarParseErrorHandler } from './ts'; 12 | 13 | export const defaultErrorHandler: ScalarParseErrorHandler = ({ 14 | code, 15 | ast, 16 | }): never => { 17 | throw new GraphQLError(`code=${code}`, ast ? [ast] : []); 18 | }; 19 | 20 | export const defaultSerialize = (x: any): any => x; 21 | 22 | export const getValueFromValueNode = (ast: ValueNode): any => { 23 | switch (ast.kind) { 24 | case Kind.BOOLEAN: 25 | return (ast as BooleanValueNode).value; 26 | case Kind.FLOAT: 27 | return parseFloat((ast as FloatValueNode).value); 28 | case Kind.INT: 29 | return parseInt((ast as IntValueNode).value, 10); 30 | case Kind.NULL: 31 | return null; 32 | case Kind.STRING: 33 | return (ast as StringValueNode).value; 34 | case Kind.ENUM: 35 | return (ast as EnumValueNode).value; 36 | } 37 | return undefined; 38 | }; 39 | -------------------------------------------------------------------------------- /src/float.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFloat, GraphQLObjectType, GraphQLSchema } from 'graphql'; 2 | import { assertEqual, assertThrow } from './common.test'; 3 | import { createFloatScalar, IFloatScalarConfig, isSafeFloat } from './float'; 4 | 5 | test('isSafeFloat', () => { 6 | expect(isSafeFloat(0)).toBe(true); 7 | expect(isSafeFloat(1)).toBe(true); 8 | expect(isSafeFloat(-1)).toBe(true); 9 | expect(isSafeFloat(null)).toBe(false); 10 | expect(isSafeFloat(true)).toBe(false); 11 | expect(isSafeFloat('')).toBe(false); 12 | expect(isSafeFloat('0')).toBe(false); 13 | expect(isSafeFloat(1.1)).toBe(true); 14 | expect(isSafeFloat(Number.NaN)).toBe(false); 15 | expect(isSafeFloat(Number.POSITIVE_INFINITY)).toBe(false); 16 | expect(isSafeFloat(Number.NEGATIVE_INFINITY)).toBe(false); 17 | }); 18 | 19 | const getSchema = (options: IFloatScalarConfig): GraphQLSchema => 20 | new GraphQLSchema({ 21 | query: new GraphQLObjectType({ 22 | name: 'Query', 23 | fields: { 24 | input: { 25 | type: GraphQLFloat, 26 | args: { 27 | value: { 28 | type: createFloatScalar(options), 29 | }, 30 | }, 31 | resolve: (_, { value }): any => value, 32 | }, 33 | }, 34 | }), 35 | }); 36 | 37 | describe('GraphQLInputFloat', () => { 38 | it('default', async () => { 39 | const schema = getSchema({ 40 | name: 'float', 41 | }); 42 | const value = 3.1; 43 | const expected = value; 44 | await assertEqual(schema, value, expected); 45 | }); 46 | 47 | it('coerce', async () => { 48 | const schema = getSchema({ 49 | name: 'float', 50 | coerce: parseFloat as any, 51 | }); 52 | await assertEqual(schema, '3.1', 3.1); 53 | }); 54 | 55 | it('coerce to null', async () => { 56 | const schema = getSchema({ 57 | name: 'float', 58 | coerce: (): null => null, 59 | }); 60 | await assertEqual(schema, '3.1', null); 61 | }); 62 | 63 | it('sanitize', async () => { 64 | const schema = getSchema({ 65 | name: 'float', 66 | sanitize: (x): number => 2 * x, 67 | }); 68 | 69 | const value = 3.1; 70 | const expected = 6.2; 71 | 72 | await assertEqual(schema, value, expected); 73 | }); 74 | 75 | it('sanitize to null', async () => { 76 | const schema = getSchema({ 77 | name: 'float', 78 | sanitize: (): null => null, 79 | }); 80 | 81 | await assertEqual(schema, 1, null); 82 | }); 83 | 84 | it('non-float bad', async () => { 85 | const schema = getSchema({ 86 | name: 'float', 87 | }); 88 | 89 | const value = '3.1'; 90 | 91 | await assertThrow(schema, value, /type/i); 92 | }); 93 | 94 | it('non-float ok', async () => { 95 | const schema = getSchema({ 96 | name: 'float', 97 | }); 98 | 99 | const value = 3.1; 100 | 101 | await assertEqual(schema, value, value); 102 | }); 103 | 104 | it('minimum bad', async () => { 105 | const schema = getSchema({ 106 | name: 'float', 107 | minimum: 3, 108 | }); 109 | 110 | const value = 2.9; 111 | 112 | await assertThrow(schema, value, /minimum/i); 113 | }); 114 | 115 | it('minimum ok', async () => { 116 | const schema = getSchema({ 117 | name: 'float', 118 | minimum: 3, 119 | }); 120 | 121 | const value = 3.1; 122 | 123 | await assertEqual(schema, value, value); 124 | }); 125 | 126 | it('maximum bad', async () => { 127 | const schema = getSchema({ 128 | name: 'float', 129 | maximum: 5, 130 | }); 131 | 132 | const value = 5.1; 133 | 134 | await assertThrow(schema, value, /maximum/i); 135 | }); 136 | 137 | it('maximum ok', async () => { 138 | const schema = getSchema({ 139 | name: 'float', 140 | maximum: 5, 141 | }); 142 | 143 | const value = 4.9; 144 | 145 | await assertEqual(schema, value, value); 146 | }); 147 | 148 | it('validate bad', async () => { 149 | const schema = getSchema({ 150 | name: 'float', 151 | validate: (x): boolean => x < 3, 152 | }); 153 | 154 | const value = 3.1; 155 | 156 | await assertThrow(schema, value, /validate/i); 157 | }); 158 | 159 | it('validate ok', async () => { 160 | const schema = getSchema({ 161 | name: 'float', 162 | validate: (x): boolean => x < 3, 163 | }); 164 | 165 | const value = 2.9; 166 | 167 | await assertEqual(schema, value, value); 168 | }); 169 | 170 | it('errorHandler', async () => { 171 | const schema = getSchema({ 172 | name: 'float', 173 | minimum: 3, 174 | errorHandler: (err: any): number => err.value - 3, 175 | }); 176 | 177 | const value = 2; 178 | 179 | await assertEqual(schema, value, -1); 180 | }); 181 | 182 | it('parse', async () => { 183 | const schema = getSchema({ 184 | name: 'float', 185 | maximum: 5, 186 | parse: (x): number => 2 * x, 187 | }); 188 | 189 | const value = 3.1; 190 | const expected = 6.2; 191 | 192 | await assertEqual(schema, value, expected); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /src/float.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { ValueNode } from 'graphql/language'; 3 | import { 4 | defaultErrorHandler, 5 | defaultSerialize, 6 | getValueFromValueNode, 7 | } from './common'; 8 | import { IScalarConfig } from './ts'; 9 | 10 | // https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js 11 | 12 | export type FloatScalarErrorCode = 'type' | 'minimum' | 'maximum' | 'validate'; 13 | 14 | export interface IFloatScalarConfig 15 | extends IScalarConfig< 16 | IFloatScalarConfig, 17 | FloatScalarErrorCode, 18 | number, 19 | TInternal, 20 | TExternal 21 | > { 22 | maximum?: number; 23 | minimum?: number; 24 | } 25 | 26 | export const isSafeFloat = (n: unknown): n is number => 27 | typeof n === 'number' && isFinite(n); 28 | 29 | export const createFloatScalar = ( 30 | config: IFloatScalarConfig 31 | ): GraphQLScalarType => { 32 | const { 33 | coerce, 34 | errorHandler, 35 | maximum, 36 | minimum, 37 | parse, 38 | sanitize, 39 | validate, 40 | serialize, 41 | ...scalarConfig 42 | } = config; 43 | 44 | const handleError = errorHandler || defaultErrorHandler; 45 | 46 | const parseValue = ( 47 | unknownValue: unknown, 48 | ast?: ValueNode 49 | ): TInternal | null => { 50 | // null inputs don't come here 51 | // Coersion Phase 52 | 53 | if (unknownValue == null) { 54 | return null; 55 | } 56 | 57 | let value: number; 58 | if (isSafeFloat(unknownValue)) { 59 | value = unknownValue; 60 | } else { 61 | if (coerce) { 62 | const valueOrNull = coerce(unknownValue); 63 | if (valueOrNull == null) { 64 | return null; 65 | } 66 | value = valueOrNull; 67 | } else { 68 | return handleError({ 69 | code: 'type', 70 | originalValue: unknownValue, 71 | value: unknownValue, 72 | ast, 73 | config, 74 | }); 75 | } 76 | } 77 | 78 | // Sanitization Phase 79 | 80 | if (sanitize && value != null) { 81 | const valueOrNull = sanitize(value); 82 | if (valueOrNull == null) { 83 | return null; 84 | } 85 | value = valueOrNull; 86 | } 87 | 88 | // Validation Phase 89 | 90 | if (minimum != null && value < minimum) { 91 | return handleError({ 92 | code: 'minimum', 93 | originalValue: unknownValue, 94 | value, 95 | ast, 96 | config, 97 | }); 98 | } 99 | 100 | if (maximum != null && value > maximum) { 101 | return handleError({ 102 | code: 'maximum', 103 | originalValue: unknownValue, 104 | value, 105 | ast, 106 | config, 107 | }); 108 | } 109 | 110 | if (validate && !validate(value)) { 111 | return handleError({ 112 | code: 'validate', 113 | originalValue: unknownValue, 114 | value, 115 | ast, 116 | config, 117 | }); 118 | } 119 | 120 | // Parse Phase 121 | 122 | if (parse) { 123 | return parse(value); 124 | } 125 | 126 | return value as any; 127 | }; 128 | 129 | return new GraphQLScalarType({ 130 | ...scalarConfig, 131 | serialize: serialize || defaultSerialize, 132 | parseValue, 133 | parseLiteral: (ast): TInternal | null => 134 | parseValue(getValueFromValueNode(ast), ast), 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | defaultErrorHandler, 3 | defaultSerialize, 4 | getValueFromValueNode, 5 | } from './common'; 6 | export { 7 | FloatScalarErrorCode, 8 | IFloatScalarConfig, 9 | createFloatScalar, 10 | isSafeFloat, 11 | } from './float'; 12 | export { 13 | IIntScalarConfig, 14 | IntScalarErrorCode, 15 | createIntScalar, 16 | isSafeInteger, 17 | } from './int'; 18 | export { 19 | IStringScalarConfig, 20 | StringScalarErrorCode, 21 | createStringScalar, 22 | } from './string'; 23 | export { 24 | IScalarConfig, 25 | IScalarParseError, 26 | ScalarCoerceFunction, 27 | ScalarParseErrorHandler, 28 | ScalarParseFunction, 29 | ScalarSanitizeFunction, 30 | ScalarSerializeFunction, 31 | ScalarValidateFunction, 32 | } from './ts'; 33 | -------------------------------------------------------------------------------- /src/int.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql'; 2 | import { assertEqual, assertThrow } from './common.test'; 3 | import { createIntScalar, IIntScalarConfig, isSafeInteger } from './int'; 4 | 5 | const getSchema = (options: IIntScalarConfig): GraphQLSchema => 6 | new GraphQLSchema({ 7 | query: new GraphQLObjectType({ 8 | name: 'Query', 9 | fields: { 10 | input: { 11 | type: GraphQLInt, 12 | args: { 13 | value: { 14 | type: createIntScalar(options), 15 | }, 16 | }, 17 | resolve: (_, { value }): any => value, 18 | }, 19 | }, 20 | }), 21 | }); 22 | 23 | test('isSafeInteger', () => { 24 | expect(isSafeInteger(0)).toBe(true); 25 | expect(isSafeInteger(1)).toBe(true); 26 | expect(isSafeInteger(-1)).toBe(true); 27 | expect(isSafeInteger(null)).toBe(false); 28 | expect(isSafeInteger(true)).toBe(false); 29 | expect(isSafeInteger('')).toBe(false); 30 | expect(isSafeInteger('0')).toBe(false); 31 | expect(isSafeInteger(1.1)).toBe(false); 32 | expect(isSafeInteger(Number.NaN)).toBe(false); 33 | expect(isSafeInteger(Number.POSITIVE_INFINITY)).toBe(false); 34 | expect(isSafeInteger(Number.NEGATIVE_INFINITY)).toBe(false); 35 | }); 36 | 37 | describe('GraphQLInputInt', () => { 38 | it('default', async () => { 39 | const schema = getSchema({ 40 | name: 'int', 41 | }); 42 | const value = 3; 43 | const expected = value; 44 | await assertEqual(schema, value, expected); 45 | }); 46 | 47 | it('coerce', async () => { 48 | const schema = getSchema({ 49 | name: 'int', 50 | coerce: parseInt as any, 51 | }); 52 | await assertEqual(schema, '3', 3); 53 | }); 54 | 55 | it('coerce to null', async () => { 56 | const schema = getSchema({ 57 | name: 'int', 58 | coerce: (): null => null, 59 | }); 60 | await assertEqual(schema, '3', null); 61 | }); 62 | 63 | it('sanitize', async () => { 64 | const schema = getSchema({ 65 | name: 'int', 66 | sanitize: (x): number => 2 * x, 67 | }); 68 | const value = 3; 69 | const expected = 6; 70 | await assertEqual(schema, value, expected); 71 | }); 72 | 73 | it('sanitize to null', async () => { 74 | const schema = getSchema({ 75 | name: 'int', 76 | sanitize: (): null => null, 77 | }); 78 | await assertEqual(schema, 1, null); 79 | }); 80 | 81 | it('non-int bad', async () => { 82 | const schema = getSchema({ 83 | name: 'int', 84 | }); 85 | const value = '3'; 86 | await assertThrow(schema, value, /type/i); 87 | }); 88 | 89 | it('non-int ok', async () => { 90 | const schema = getSchema({ 91 | name: 'int', 92 | }); 93 | const value = 3; 94 | await assertEqual(schema, value, value); 95 | }); 96 | 97 | it('float bad', async () => { 98 | const schema = getSchema({ 99 | name: 'int', 100 | }); 101 | const value = 3.5; 102 | await assertThrow(schema, value, /type/i); 103 | }); 104 | 105 | it('minimum bad', async () => { 106 | const schema = getSchema({ 107 | name: 'int', 108 | minimum: 3, 109 | }); 110 | const value = 2; 111 | await assertThrow(schema, value, /minimum/i); 112 | }); 113 | 114 | it('minimum ok', async () => { 115 | const schema = getSchema({ 116 | name: 'int', 117 | minimum: 3, 118 | }); 119 | const value = 3; 120 | await assertEqual(schema, value, value); 121 | }); 122 | 123 | it('maximum bad', async () => { 124 | const schema = getSchema({ 125 | name: 'int', 126 | maximum: 5, 127 | }); 128 | const value = 6; 129 | await assertThrow(schema, value, /maximum/i); 130 | }); 131 | 132 | it('maximum ok', async () => { 133 | const schema = getSchema({ 134 | name: 'int', 135 | maximum: 5, 136 | }); 137 | const value = 5; 138 | await assertEqual(schema, value, value); 139 | }); 140 | 141 | it('validate bad', async () => { 142 | const schema = getSchema({ 143 | name: 'int', 144 | validate: (x): boolean => x < 3, 145 | }); 146 | const value = 3; 147 | await assertThrow(schema, value, /validate/i); 148 | }); 149 | 150 | it('validate ok', async () => { 151 | const schema = getSchema({ 152 | name: 'int', 153 | validate: (x): boolean => x < 3, 154 | }); 155 | const value = 2; 156 | await assertEqual(schema, value, value); 157 | }); 158 | 159 | it('errorHandler', async () => { 160 | const schema = getSchema({ 161 | name: 'int', 162 | minimum: 3, 163 | errorHandler: (err: any): number => err.value - 3, 164 | }); 165 | const value = 2; 166 | await assertEqual(schema, value, -1); 167 | }); 168 | 169 | it('parse', async () => { 170 | const schema = getSchema({ 171 | name: 'int', 172 | maximum: 5, 173 | parse: (x): number => 2 * x, 174 | }); 175 | const value = 3; 176 | const expected = 6; 177 | await assertEqual(schema, value, expected); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/int.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { ValueNode } from 'graphql/language'; 3 | import { 4 | defaultErrorHandler, 5 | defaultSerialize, 6 | getValueFromValueNode, 7 | } from './common'; 8 | import { IScalarConfig } from './ts'; 9 | 10 | export type IntScalarErrorCode = 'type' | 'minimum' | 'maximum' | 'validate'; 11 | 12 | export interface IIntScalarConfig 13 | extends IScalarConfig< 14 | IIntScalarConfig, 15 | IntScalarErrorCode, 16 | number, 17 | TInternal, 18 | TExternal 19 | > { 20 | maximum?: number; 21 | minimum?: number; 22 | } 23 | 24 | // https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js 25 | const MAX_INT = 2147483647; 26 | const MIN_INT = -2147483648; 27 | 28 | export const isSafeInteger = (n: unknown): n is number => 29 | typeof n === 'number' && 30 | isFinite(n) && 31 | Math.floor(n) === n && 32 | n <= MAX_INT && 33 | n >= MIN_INT; 34 | 35 | export const createIntScalar = ( 36 | config: IIntScalarConfig 37 | ): GraphQLScalarType => { 38 | const { 39 | coerce, 40 | errorHandler, 41 | maximum, 42 | minimum, 43 | parse, 44 | sanitize, 45 | validate, 46 | serialize, 47 | ...scalarConfig 48 | } = config; 49 | 50 | const handleError = errorHandler || defaultErrorHandler; 51 | 52 | const parseValue = ( 53 | unknownValue: unknown, 54 | ast?: ValueNode 55 | ): TInternal | null => { 56 | // null inputs don't come here 57 | // Coersion Phase 58 | 59 | if (unknownValue == null) { 60 | return null; 61 | } 62 | 63 | let value: number; 64 | if (isSafeInteger(unknownValue)) { 65 | value = unknownValue; 66 | } else { 67 | if (coerce) { 68 | const valueOrNull = coerce(unknownValue); 69 | if (valueOrNull == null) { 70 | return null; 71 | } 72 | value = valueOrNull; 73 | } else { 74 | return handleError({ 75 | code: 'type', 76 | originalValue: unknownValue, 77 | value: unknownValue, 78 | ast, 79 | config, 80 | }); 81 | } 82 | } 83 | 84 | // Sanitization Phase 85 | 86 | if (sanitize && value != null) { 87 | const valueOrNull = sanitize(value); 88 | if (valueOrNull == null) { 89 | return null; 90 | } 91 | value = valueOrNull; 92 | } 93 | 94 | // Validation Phase 95 | 96 | if (minimum != null && value < minimum) { 97 | return handleError({ 98 | code: 'minimum', 99 | originalValue: unknownValue, 100 | value, 101 | ast, 102 | config, 103 | }); 104 | } 105 | 106 | if (maximum != null && value > maximum) { 107 | return handleError({ 108 | code: 'maximum', 109 | originalValue: unknownValue, 110 | value, 111 | ast, 112 | config, 113 | }); 114 | } 115 | 116 | if (validate && !validate(value)) { 117 | return handleError({ 118 | code: 'validate', 119 | originalValue: unknownValue, 120 | value, 121 | ast, 122 | config, 123 | }); 124 | } 125 | 126 | // Parse Phase 127 | 128 | if (parse) { 129 | return parse(value); 130 | } 131 | 132 | return value as any; 133 | }; 134 | 135 | return new GraphQLScalarType({ 136 | ...scalarConfig, 137 | serialize: serialize || defaultSerialize, 138 | parseValue, 139 | parseLiteral: (ast): TInternal | null => 140 | parseValue(getValueFromValueNode(ast), ast), 141 | }); 142 | }; 143 | -------------------------------------------------------------------------------- /src/string.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | graphql, 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | GraphQLString, 6 | } from 'graphql'; 7 | import { assertEqual, assertThrow } from './common.test'; 8 | import { createStringScalar, IStringScalarConfig } from './string'; 9 | 10 | const getSchema = (options: IStringScalarConfig): GraphQLSchema => 11 | new GraphQLSchema({ 12 | query: new GraphQLObjectType({ 13 | name: 'Query', 14 | fields: { 15 | input: { 16 | type: GraphQLString, 17 | args: { 18 | value: { 19 | type: createStringScalar(options), 20 | }, 21 | }, 22 | resolve: (_, { value }): any => value, 23 | }, 24 | }, 25 | }), 26 | }); 27 | 28 | describe('GraphQLString', () => { 29 | it('basic', async () => { 30 | const schema = getSchema({ name: 'string' }); 31 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 32 | await assertEqual(schema, value, value); 33 | }); 34 | 35 | it('null', async () => { 36 | const schema = getSchema({ name: 'string' }); 37 | await assertEqual(schema, null, null); 38 | }); 39 | 40 | it('coerce', async () => { 41 | const schema = getSchema({ 42 | name: 'string', 43 | coerce: (x: any): any => x || null, 44 | }); 45 | await assertEqual(schema, 3, '3'); 46 | await assertEqual(schema, 0, null); 47 | }); 48 | 49 | it('trim', async () => { 50 | const schema = getSchema({ 51 | name: 'string', 52 | trim: true, 53 | }); 54 | 55 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 56 | 57 | await assertEqual(schema, value, value.trim()); 58 | }); 59 | 60 | it('trimLeft', async () => { 61 | const schema = getSchema({ 62 | name: 'string', 63 | trimLeft: true, 64 | }); 65 | 66 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 67 | 68 | await assertEqual(schema, value, value.trimLeft()); 69 | }); 70 | 71 | it('trimRight', async () => { 72 | const schema = getSchema({ 73 | name: 'string', 74 | trimRight: true, 75 | }); 76 | 77 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 78 | 79 | await assertEqual(schema, value, value.trimRight()); 80 | }); 81 | 82 | it('singleline', async () => { 83 | const schema = getSchema({ 84 | name: 'string', 85 | singleline: '.', 86 | }); 87 | 88 | const value = '\nHello\n World\n \nHi'; 89 | 90 | await assertEqual(schema, value, '.Hello. World. .Hi'); 91 | }); 92 | 93 | it('collapseWhitespace', async () => { 94 | const schema = getSchema({ 95 | name: 'string', 96 | collapseWhitespace: true, 97 | }); 98 | 99 | const value = ' Hello \n World\n '; 100 | 101 | await assertEqual(schema, value, ' Hello\nWorld\n'); 102 | }); 103 | 104 | it('collapseWhitespaceSingleline', async () => { 105 | const schema = getSchema({ 106 | name: 'string', 107 | collapseWhitespace: true, 108 | singleline: ' ', 109 | }); 110 | 111 | const value = ' \n\t\r Hello \n World'; 112 | const expected = ' Hello World'; 113 | 114 | await assertEqual(schema, value, expected); 115 | }); 116 | 117 | it('empty bad', async () => { 118 | const schema = getSchema({ 119 | name: 'string', 120 | nonEmpty: true, 121 | }); 122 | 123 | const value = ''; 124 | await assertThrow(schema, value, /empty/i); 125 | }); 126 | 127 | it('empty ok', async () => { 128 | const schema = getSchema({ 129 | name: 'string', 130 | }); 131 | 132 | const value = ''; 133 | 134 | await assertEqual(schema, value, value); 135 | }); 136 | 137 | it('truncate', async () => { 138 | const schema = getSchema({ 139 | name: 'string', 140 | truncate: 10, 141 | }); 142 | 143 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 144 | const expected = value.substring(0, 10); 145 | 146 | await assertEqual(schema, value, expected); 147 | }); 148 | 149 | it('truncate no effect', async () => { 150 | const schema = getSchema({ 151 | name: 'string', 152 | truncate: 10, 153 | }); 154 | 155 | const value = ' 921hlu'; 156 | const expected = value; 157 | 158 | await assertEqual(schema, value, expected); 159 | }); 160 | 161 | it('trim and truncate', async () => { 162 | const schema = getSchema({ 163 | name: 'string', 164 | trim: true, 165 | truncate: 10, 166 | }); 167 | 168 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 169 | const expected = value.trim().substring(0, 10); 170 | 171 | await assertEqual(schema, value, expected); 172 | }); 173 | 174 | it('upperCase', async () => { 175 | const schema = getSchema({ 176 | name: 'string', 177 | uppercase: true, 178 | }); 179 | 180 | const value = ' 921hluAoCb1 au0[g2930,0.Uh, '; 181 | const expected = value.toUpperCase(); 182 | 183 | await assertEqual(schema, value, expected); 184 | }); 185 | 186 | it('lowerCase', async () => { 187 | const schema = getSchema({ 188 | name: 'string', 189 | lowercase: true, 190 | }); 191 | 192 | const value = ' 921HluAOCB1 au0[G2930,0.uh, '; 193 | const expected = value.toLowerCase(); 194 | 195 | await assertEqual(schema, value, expected); 196 | }); 197 | 198 | it('capitalize', async () => { 199 | const schema = getSchema({ 200 | name: 'string', 201 | capitalize: 'first', 202 | }); 203 | 204 | const value = 'hello my friend.'; 205 | const expected = 'Hello my friend.'; 206 | 207 | await assertEqual(schema, value, expected); 208 | }); 209 | 210 | it('capitalize characters', async () => { 211 | const schema = getSchema({ 212 | name: 'string', 213 | capitalize: 'characters', 214 | }); 215 | 216 | const value = 'hello my friend.'; 217 | const expected = 'HELLO MY FRIEND.'; 218 | 219 | await assertEqual(schema, value, expected); 220 | }); 221 | 222 | it('capitalize words', async () => { 223 | const schema = getSchema({ 224 | name: 'string', 225 | capitalize: 'words', 226 | }); 227 | 228 | const value = 'hello my friend.'; 229 | const expected = 'Hello My Friend.'; 230 | 231 | await assertEqual(schema, value, expected); 232 | }); 233 | 234 | it('capitalize sentences', async () => { 235 | const schema = getSchema({ 236 | name: 'string', 237 | capitalize: 'sentences', 238 | }); 239 | 240 | const value = 'hello my friend. hello my friend.'; 241 | const expected = 'Hello my friend. Hello my friend.'; 242 | 243 | await assertEqual(schema, value, expected); 244 | }); 245 | 246 | it('capitalize empty string', async () => { 247 | const schema = getSchema({ 248 | name: 'string', 249 | capitalize: 'first', 250 | }); 251 | 252 | const value = ''; 253 | const expected = ''; 254 | 255 | await assertEqual(schema, value, expected); 256 | }); 257 | 258 | it('trim and capitalize', async () => { 259 | const schema = getSchema({ 260 | name: 'string', 261 | trim: true, 262 | capitalize: 'first', 263 | }); 264 | 265 | const value = ' hello my friend. '; 266 | const expected = 'Hello my friend.'; 267 | 268 | await assertEqual(schema, value, expected); 269 | }); 270 | 271 | it('sanitize', async () => { 272 | const schema = getSchema({ 273 | name: 'string', 274 | sanitize: (s): string => s.replace(/[^\d]*/g, ''), 275 | }); 276 | 277 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 278 | const expected = '9211029300'; 279 | 280 | await assertEqual(schema, value, expected); 281 | }); 282 | 283 | it('non-string bad', async () => { 284 | const schema = getSchema({ 285 | name: 'string', 286 | }); 287 | 288 | await assertThrow(schema, true, /type/i); 289 | await assertThrow(schema, false, /type/i); 290 | await assertThrow(schema, 3, /type/i); 291 | await assertThrow(schema, 3.5, /type/i); 292 | }); 293 | 294 | it('sanitize to null', async () => { 295 | const schema = getSchema({ 296 | name: 'string', 297 | sanitize: (x): string | null => x || null, 298 | }); 299 | const value = ''; 300 | await assertEqual(schema, value, null); 301 | }); 302 | 303 | it('non-null error', async () => { 304 | const schema = getSchema({ 305 | name: 'string', 306 | sanitize: (x): string | null => x || null, 307 | }); 308 | const value = ''; 309 | await assertEqual(schema, value, null); 310 | }); 311 | 312 | it('non-string ok', async () => { 313 | const schema = getSchema({ 314 | name: 'string', 315 | }); 316 | 317 | const value = '3'; 318 | 319 | await assertEqual(schema, value, value); 320 | }); 321 | 322 | it('maxEmptyLines', async () => { 323 | const schema = getSchema({ 324 | name: 'string', 325 | trim: true, 326 | collapseWhitespace: true, 327 | maxEmptyLines: 1, 328 | }); 329 | 330 | const value = 331 | '\n\n a \t\n \n \n \n b \n c\r\n\r\nd\r\n\n\ne\r\r\r\rf \n\n'; 332 | 333 | await assertEqual(schema, value, 'a\n\nb\nc\n\nd\n\ne\n\nf'); 334 | }); 335 | 336 | it('minLength bad', async () => { 337 | const schema = getSchema({ 338 | name: 'string', 339 | minLength: 3, 340 | }); 341 | 342 | const value = 'ab'; 343 | 344 | await assertThrow(schema, value, /minLength/i); 345 | }); 346 | 347 | it('minLength ok', async () => { 348 | const schema = getSchema({ 349 | name: 'string', 350 | minLength: 3, 351 | }); 352 | 353 | const value = 'abc'; 354 | 355 | await assertEqual(schema, value, value); 356 | }); 357 | 358 | it('maxLength bad', async () => { 359 | const schema = getSchema({ 360 | name: 'string', 361 | maxLength: 5, 362 | }); 363 | 364 | const value = 'abcdef'; 365 | 366 | await assertThrow(schema, value, /maxLength/i); 367 | }); 368 | 369 | it('maxLength ok', async () => { 370 | const schema = getSchema({ 371 | name: 'string', 372 | maxLength: 5, 373 | }); 374 | 375 | const value = 'abcde'; 376 | 377 | await assertEqual(schema, value, value); 378 | }); 379 | 380 | it('pattern bad', async () => { 381 | const schema = getSchema({ 382 | name: 'string', 383 | pattern: /^\w+$/, 384 | }); 385 | 386 | const value = ' a '; 387 | 388 | await assertThrow(schema, value, /pattern/i); 389 | }); 390 | 391 | it('pattern ok', async () => { 392 | const schema = getSchema({ 393 | name: 'string', 394 | pattern: /^\w+$/, 395 | }); 396 | 397 | const value = 'abc'; 398 | 399 | await assertEqual(schema, value, value); 400 | }); 401 | 402 | it('pattern string ok', async () => { 403 | const schema = getSchema({ 404 | name: 'string', 405 | pattern: '^\\w+$', 406 | }); 407 | 408 | const value = 'abc'; 409 | 410 | await assertEqual(schema, value, value); 411 | }); 412 | 413 | it('validate bad', async () => { 414 | const schema = getSchema({ 415 | name: 'string', 416 | validate: (x): boolean => x.length < 3, 417 | }); 418 | 419 | const value = 'abc'; 420 | 421 | await assertThrow(schema, value, /validate/i); 422 | }); 423 | 424 | it('validate ok', async () => { 425 | const schema = getSchema({ 426 | name: 'string', 427 | validate: (x): boolean => x.length < 3, 428 | }); 429 | 430 | const value = 'ab'; 431 | 432 | await assertEqual(schema, value, value); 433 | }); 434 | 435 | it('parse', async () => { 436 | const schema = getSchema({ 437 | name: 'string', 438 | minLength: 5, // not forced to parse. 439 | parse: (s): string => s.substring(0, 3), 440 | }); 441 | 442 | const value = ' 921hluaocb1 au0[g2930,0.uh, '; 443 | const expected = value.substring(0, 3); 444 | 445 | await assertEqual(schema, value, expected); 446 | }); 447 | 448 | it('errorHandler', async () => { 449 | const schema = getSchema({ 450 | name: 'string', 451 | errorHandler: (): string => 'there was error', 452 | }); 453 | 454 | const value = 4; 455 | 456 | await assertEqual(schema, value, 'there was error'); 457 | }); 458 | 459 | it('description', () => { 460 | const description = 'this is description'; 461 | const type = createStringScalar({ 462 | name: 'string', 463 | description, 464 | }); 465 | expect(type.description).toEqual(description); 466 | }); 467 | 468 | it('serialize', async () => { 469 | const schema = new GraphQLSchema({ 470 | query: new GraphQLObjectType({ 471 | name: 'Query', 472 | fields: { 473 | output: { 474 | type: createStringScalar({ 475 | name: 'string', 476 | trim: true, 477 | }), 478 | resolve: (): string => ' test ', 479 | }, 480 | }, 481 | }), 482 | }); 483 | 484 | const res = await graphql(schema, '{ output }'); 485 | 486 | // trim is only applied to input 487 | expect(res.data!.output).toEqual(' test '); 488 | }); 489 | }); 490 | -------------------------------------------------------------------------------- /src/string.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { ValueNode } from 'graphql/language'; 3 | import { 4 | defaultErrorHandler, 5 | defaultSerialize, 6 | getValueFromValueNode, 7 | } from './common'; 8 | import { IScalarConfig } from './ts'; 9 | 10 | export type StringScalarErrorCode = 11 | | 'type' 12 | | 'empty' 13 | | 'minLength' 14 | | 'maxLength' 15 | | 'pattern' 16 | | 'validate'; 17 | 18 | export interface IStringScalarConfig 19 | extends IScalarConfig< 20 | IStringScalarConfig, 21 | StringScalarErrorCode, 22 | string, 23 | TInternal, 24 | TExternal 25 | > { 26 | capitalize?: 'characters' | 'words' | 'sentences' | 'first'; 27 | collapseWhitespace?: boolean; 28 | lowercase?: boolean; 29 | maxEmptyLines?: number; 30 | maxLength?: number; 31 | minLength?: number; 32 | nonEmpty?: boolean; 33 | pattern?: RegExp | string; 34 | singleline?: string; 35 | trim?: boolean; 36 | trimLeft?: boolean; 37 | trimRight?: boolean; 38 | truncate?: number; 39 | uppercase?: boolean; 40 | } 41 | 42 | const strToUpperCase = (str: string): string => str.toUpperCase(); 43 | 44 | const wordRegex = /(?:^|\s)\S/g; 45 | 46 | const sentenceRegex = /(?:^|\.\s)\S/g; 47 | 48 | const newlineRegex = /[\r\n]+/g; 49 | 50 | const newlineWithWSRegex = /\s*[\r\n]+\s*/g; 51 | 52 | const linebreakRegex = /\r\n|\r|\n/g; 53 | 54 | const whitespace = /\s+/g; 55 | 56 | const collapseWS = (str: string): string => str.replace(whitespace, ' '); 57 | 58 | const trimAndCollapseWS = (str: string): string => 59 | str.trim().replace(whitespace, ' '); 60 | 61 | export const createStringScalar = ( 62 | config: IStringScalarConfig 63 | ): GraphQLScalarType => { 64 | const { 65 | capitalize, 66 | coerce, 67 | collapseWhitespace, 68 | errorHandler, 69 | lowercase, 70 | maxEmptyLines, 71 | maxLength, 72 | minLength, 73 | nonEmpty, 74 | parse, 75 | pattern, 76 | sanitize, 77 | serialize, 78 | singleline, 79 | trim, 80 | trimLeft, 81 | trimRight, 82 | truncate, 83 | uppercase, 84 | validate, 85 | ...scalarConfig 86 | } = config; 87 | 88 | const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern; 89 | 90 | const handleError = errorHandler || defaultErrorHandler; 91 | 92 | let emptyLineRegex: RegExp | null = null; 93 | let emptyLineString: string | null = null; 94 | if (maxEmptyLines) { 95 | emptyLineRegex = new RegExp(`\n{${maxEmptyLines + 2},}`, 'g'); 96 | emptyLineString = '\n'.repeat(maxEmptyLines + 1); 97 | } 98 | 99 | const parseValue = ( 100 | unknownValue: unknown, 101 | ast?: ValueNode 102 | ): TInternal | null => { 103 | // null inputs don't come here 104 | // Coersion Phase 105 | 106 | if (unknownValue == null) { 107 | return null; 108 | } 109 | 110 | let value: string; 111 | if (typeof unknownValue === 'string') { 112 | value = unknownValue; 113 | } else { 114 | if (coerce) { 115 | const valueOrNull = coerce(unknownValue); 116 | if (valueOrNull == null) { 117 | return null; 118 | } 119 | value = valueOrNull; 120 | } else { 121 | return handleError({ 122 | code: 'type', 123 | originalValue: unknownValue, 124 | value: unknownValue, 125 | ast, 126 | config, 127 | }); 128 | } 129 | } 130 | 131 | // Sanitization Phase 132 | 133 | if (value) { 134 | if (trim) { 135 | value = value.trim(); 136 | } else { 137 | if (trimLeft) { 138 | value = value.trimLeft(); 139 | } 140 | if (trimRight) { 141 | value = value.trimRight(); 142 | } 143 | } 144 | 145 | if (value) { 146 | if (singleline) { 147 | value = value.replace(newlineRegex, singleline); 148 | } 149 | 150 | if (collapseWhitespace) { 151 | if (singleline) { 152 | // newlines replaced already 153 | value = value.replace(whitespace, ' '); 154 | } else if (maxEmptyLines) { 155 | value = value 156 | .split(linebreakRegex) 157 | .map(trimAndCollapseWS) 158 | .join('\n') 159 | .replace(emptyLineRegex!, emptyLineString!); 160 | } else { 161 | value = value.split(newlineWithWSRegex).map(collapseWS).join('\n'); 162 | } 163 | } 164 | 165 | if (truncate != null && value.length > truncate) { 166 | value = value.substring(0, truncate); 167 | } 168 | 169 | if (uppercase) { 170 | value = value.toUpperCase(); 171 | } else if (lowercase) { 172 | value = value.toLowerCase(); 173 | } 174 | 175 | if (capitalize) { 176 | switch (capitalize) { 177 | case 'characters': 178 | value = value.toUpperCase(); 179 | break; 180 | case 'words': 181 | value = value.replace(wordRegex, strToUpperCase); 182 | break; 183 | case 'sentences': 184 | value = value.replace(sentenceRegex, strToUpperCase); 185 | break; 186 | case 'first': 187 | default: 188 | value = value[0].toUpperCase() + value.slice(1); 189 | break; 190 | } 191 | } 192 | } 193 | } 194 | 195 | if (sanitize) { 196 | const valueOrNull = sanitize(value); 197 | if (valueOrNull == null) { 198 | return null; 199 | } 200 | value = valueOrNull; 201 | } 202 | 203 | // Validation Phase 204 | 205 | if (nonEmpty && !value) { 206 | return handleError({ 207 | code: 'empty', 208 | originalValue: unknownValue, 209 | value, 210 | ast, 211 | config, 212 | }); 213 | } 214 | 215 | if (minLength != null && value.length < minLength) { 216 | return handleError({ 217 | code: 'minLength', 218 | originalValue: unknownValue, 219 | value, 220 | ast, 221 | config, 222 | }); 223 | } 224 | 225 | if (maxLength != null && value.length > maxLength) { 226 | return handleError({ 227 | code: 'maxLength', 228 | originalValue: unknownValue, 229 | value, 230 | ast, 231 | config, 232 | }); 233 | } 234 | 235 | if (regex != null && !regex.test(value)) { 236 | return handleError({ 237 | code: 'pattern', 238 | originalValue: unknownValue, 239 | value, 240 | ast, 241 | config, 242 | }); 243 | } 244 | 245 | if (validate && !validate(value)) { 246 | return handleError({ 247 | code: 'validate', 248 | originalValue: unknownValue, 249 | value, 250 | ast, 251 | config, 252 | }); 253 | } 254 | 255 | // Parse Phase 256 | 257 | if (parse) { 258 | return parse(value); 259 | } 260 | 261 | return value as any; 262 | }; 263 | 264 | return new GraphQLScalarType({ 265 | ...scalarConfig, 266 | serialize: serialize || defaultSerialize, 267 | parseValue, 268 | parseLiteral: (ast): TInternal | null => 269 | parseValue(getValueFromValueNode(ast), ast), 270 | }); 271 | }; 272 | -------------------------------------------------------------------------------- /src/ts.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarTypeConfig, ValueNode } from 'graphql'; 2 | import { ExcludeKeys } from 'tsdef'; 3 | 4 | export interface IScalarParseError { 5 | code: TCode; 6 | originalValue: unknown; 7 | value: unknown; 8 | ast?: ValueNode; 9 | config: TConfig; 10 | } 11 | 12 | // may throw 13 | export type ScalarParseErrorHandler = ( 14 | errorInfo: IScalarParseError 15 | ) => TInternal; 16 | 17 | // coerce raw external input value into internal value 18 | export type ScalarCoerceFunction = (raw: unknown) => T | null | undefined; 19 | 20 | export type ScalarSanitizeFunction = (value: T) => T | null | undefined; 21 | 22 | export type ScalarValidateFunction = (value: T) => boolean; 23 | 24 | export type ScalarParseFunction = (value: T) => U; 25 | 26 | export type ScalarSerializeFunction = (value: T) => U; 27 | 28 | // lifecycle 29 | // coerce -> sanitize -> validate -> parse 30 | 31 | export interface IScalarConfig< 32 | TConfig, 33 | TErrorCode, 34 | TValue, 35 | TInternal, 36 | TExternal 37 | > extends ExcludeKeys< 38 | GraphQLScalarTypeConfig, 39 | 'serialize' | 'parseValue' | 'parseLiteral' 40 | > { 41 | coerce?: ScalarCoerceFunction; 42 | errorHandler?: ScalarParseErrorHandler; 43 | parse?: ScalarParseFunction; 44 | sanitize?: ScalarSanitizeFunction; 45 | serialize?: ScalarSerializeFunction; 46 | validate?: ScalarValidateFunction; 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "dist/browser", 5 | "declarationDir": "dist/browser", 6 | "lib": ["dom", "esnext"], 7 | "target": "es5", 8 | "module": "commonjs" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declaration": true, 6 | "declarationDir": "lib", 7 | "declarationMap": true, 8 | "emitDecoratorMetadata": true, 9 | "importHelpers": true, 10 | "noEmit": false, 11 | "noEmitHelpers": true, 12 | "preserveConstEnums": true, 13 | "removeComments": true, 14 | "sourceMap": true 15 | }, 16 | "exclude": ["node_modules", "src/**/*.test.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "baseUrl": ".", 5 | "diagnostics": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["esnext"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "noErrorTruncation": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noStrictGenericChecks": false, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "paths": { 22 | "_src/*": ["src/*"] 23 | }, 24 | "resolveJsonModule": true, 25 | "rootDir": "src", 26 | "skipLibCheck": true, 27 | "strict": true, 28 | "strictBindCallApply": true, 29 | "strictFunctionTypes": true, 30 | "strictNullChecks": true, 31 | "strictPropertyInitialization": true, 32 | "suppressExcessPropertyErrors": false, 33 | "suppressImplicitAnyIndexErrors": false, 34 | "target": "esnext" 35 | }, 36 | "include": ["src/**/*.ts"] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "declarationDir": "lib", 6 | "tsBuildInfoFile": "lib/tsconfig.module.tsbuildinfo", 7 | "lib": ["esnext"], 8 | "target": "es2017", 9 | "module": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "dist/node", 5 | "declarationDir": "dist/node", 6 | "lib": ["es2017"], 7 | "target": "es2017", 8 | "module": "commonjs" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-config-prettier", 5 | "tslint-config-airbnb" 6 | ], 7 | "rules": { 8 | "align": false, 9 | "array-type": [true, "array-simple"], 10 | "callable-types": false, 11 | "import-name": false, 12 | "max-line-length": false, 13 | "no-boolean-literal-compare": false, 14 | "no-empty-interface": false, 15 | "no-inferrable-types": true, 16 | "no-this-assignment": [true, { "allow-destructuring": true }], 17 | "object-literal-sort-keys": false, 18 | "object-shorthand-properties-first": false, 19 | "prefer-array-literal": false, 20 | "semicolon": [true, "always", "ignore-bound-class-methods"], 21 | "ter-arrow-parens": [true, "always"], 22 | "ter-func-call-spacing": false, 23 | "ter-indent": false, 24 | "trailing-comma": [ 25 | true, 26 | { 27 | "multiline": { 28 | "arrays": "always", 29 | "objects": "always", 30 | "functions": "never", 31 | "imports": "always", 32 | "exports": "always", 33 | "typeLiterals": "always" 34 | }, 35 | "singleline": "never", 36 | "esSpecCompliant": true 37 | } 38 | ], 39 | "typedef": [true, "call-signature", "arrow-call-signature"], 40 | "variable-name": [ 41 | true, 42 | "ban-keywords", 43 | "check-format", 44 | "allow-leading-underscore", 45 | "allow-pascal-case" 46 | ] 47 | }, 48 | "jsRules": {} 49 | } 50 | --------------------------------------------------------------------------------