├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __fixtures__ ├── schema.graphql ├── schema.json └── testSrc │ └── foo.js ├── bin ├── run └── run.cmd ├── demo.gif ├── graphql-usage-ui ├── .env ├── .gitignore ├── README.md ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── Delimiter.tsx │ ├── DetailsPanel.tsx │ ├── FieldLine.tsx │ ├── FieldName.tsx │ ├── FieldType.tsx │ ├── Header.tsx │ ├── Label.tsx │ ├── OccurrencesList.tsx │ ├── Schema.tsx │ ├── TypeBlock.tsx │ ├── TypeKind.tsx │ ├── TypeName.tsx │ ├── constants │ │ └── colors.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportTypes.ts │ └── serviceWorker.ts ├── tsconfig.json └── yarn.lock ├── jest.config.js ├── package.json ├── src ├── __snapshots__ │ └── index.test.ts.snap ├── findJSGraphQLTags.test.ts ├── findJSGraphQLTags.ts ├── findTSGraphQLTags.test.ts ├── findTSGraphQLTags.ts ├── flatten.ts ├── getFieldInfo.ts ├── gitUtils.ts ├── index.test.ts ├── index.ts ├── report.test.ts ├── report.ts ├── server.ts └── types.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | jobs: 4 | node-latest: &test 5 | docker: 6 | - image: node:latest 7 | working_directory: ~/cli 8 | steps: 9 | - checkout 10 | - restore_cache: &restore_cache 11 | keys: 12 | - v1-npm-{{checksum ".circleci/config.yml"}}-{{checksum "yarn.lock"}} 13 | - v1-npm-{{checksum ".circleci/config.yml"}} 14 | - run: &install_dependencies 15 | name: Install dependencies 16 | # --ignore-engines is necessary for Commitizen to install with Node 8 17 | command: yarn install --ignore-engines 18 | - run: ./bin/run --version 19 | - run: ./bin/run --help 20 | - run: 21 | name: Testing 22 | command: yarn test 23 | node-8: 24 | <<: *test 25 | docker: 26 | - image: node:8 27 | node-10: 28 | <<: *test 29 | docker: 30 | - image: node:10 31 | cache: 32 | <<: *test 33 | steps: 34 | - checkout 35 | - run: 36 | name: Install dependencies 37 | command: yarn 38 | - save_cache: 39 | key: v1-npm-{{checksum ".circleci/config.yml"}}-{{checksum "yarn.lock"}} 40 | paths: 41 | - ~/cli/node_modules 42 | - /usr/local/share/.cache/yarn 43 | - /usr/local/share/.config/yarn 44 | release: 45 | docker: 46 | - image: node:10 47 | steps: 48 | - checkout 49 | - restore_cache: *restore_cache 50 | - run: *install_dependencies 51 | - run: 52 | name: Install UI dependencies 53 | command: cd ./graphql-usage-ui && yarn 54 | - run: 55 | name: Build package 56 | command: yarn build 57 | - run: 58 | name: Release package 59 | command: yarn run semantic-release 60 | 61 | workflows: 62 | version: 2 63 | "graphql-usage": 64 | jobs: 65 | - node-latest 66 | - node-8 67 | - node-10 68 | - cache: 69 | filters: 70 | tags: 71 | only: /^v.*/ 72 | branches: 73 | ignore: /.*/ 74 | - release: 75 | requires: 76 | - node-8 77 | - node-10 78 | - node-latest 79 | filters: 80 | branches: 81 | only: master 82 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /build 7 | /package-lock.json 8 | /tmp 9 | node_modules 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at cdthomas.92@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2019 Drew Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-usage 2 | 3 | [![Version](https://img.shields.io/npm/v/graphql-usage.svg)](https://npmjs.org/package/graphql-usage) 4 | [![CircleCI](https://circleci.com/gh/CDThomas/graphql-usage/tree/master.svg?style=shield)](https://circleci.com/gh/CDThomas/graphql-usage/tree/master) 5 | [![License](https://img.shields.io/npm/l/graphql-usage.svg)](https://github.com/CDThomas/graphql-usage/blob/master/package.json) 6 | 7 | 🛠 A tool for refactoring GraphQL APIs. 8 | 9 | ![](/demo.gif) 10 | 11 | # Installation 12 | 13 | ## Global installation (recommended) 14 | 15 | NPM: 16 | 17 | ```bash 18 | $ npm install -g graphql-usage 19 | ``` 20 | 21 | Yarn: 22 | 23 | ```bash 24 | $ yarn global add graphql-usage 25 | ``` 26 | 27 | ## Local installation 28 | 29 | NPM: 30 | 31 | ```bash 32 | $ npm install --save-dev graphql-usage 33 | ``` 34 | 35 | Yarn: 36 | 37 | ```bash 38 | $ yarn add -D graphql-usage 39 | ``` 40 | 41 | # Support and Requirements 42 | 43 | - Source files using JS, Flow, or TypeScript 44 | - Projects using Relay, Apollo, or graphql-tag 45 | - Source files must be in a Git project and the branch that's being analyzed must be pushed to GitHub 46 | 47 | # Usage 48 | 49 | ```bash 50 | $ graphql-usage SCHEMA SOURCEDIR 51 | ``` 52 | 53 | ## Arguments: 54 | 55 | - SCHEMA: Path to the Graphql schema to report usage info for. Can be either a `.json` or `.graphql` file. 56 | - SOURCEDIR: Path to the source directory to analyze. 57 | 58 | ## Options: 59 | 60 | - `-h`, `--help`: show CLI help 61 | - `-v`, `--version`: show CLI version 62 | - `--exclude`: Directories to ignore under src 63 | - `--port`: Port to run the report server on 64 | - `--quiet`: No output to stdout 65 | 66 | ## Example: 67 | 68 | ```bash 69 | $ graphql-usage ./schema.graphql ./src/ 70 | ``` 71 | -------------------------------------------------------------------------------- /__fixtures__/schema.graphql: -------------------------------------------------------------------------------- 1 | """A character from the Star Wars universe""" 2 | interface Character { 3 | """The movies this character appears in""" 4 | appearsIn: [Episode]! 5 | 6 | """The friends of the character, or an empty list if they have none""" 7 | friends: [Character] 8 | 9 | """The friends of the character exposed as a connection with edges""" 10 | friendsConnection(first: Int, after: ID): FriendsConnection! 11 | 12 | """The ID of the character""" 13 | id: ID! 14 | 15 | """The name of the character""" 16 | name: String! 17 | } 18 | 19 | """The input object sent when passing in a color""" 20 | input ColorInput { 21 | red: Int! 22 | green: Int! 23 | blue: Int! 24 | } 25 | 26 | """An autonomous mechanical character in the Star Wars universe""" 27 | type Droid implements Character { 28 | """The movies this droid appears in""" 29 | appearsIn: [Episode]! 30 | 31 | """This droid's friends, or an empty list if they have none""" 32 | friends: [Character] 33 | 34 | """The friends of the droid exposed as a connection with edges""" 35 | friendsConnection(first: Int, after: ID): FriendsConnection! 36 | 37 | """The ID of the droid""" 38 | id: ID! 39 | 40 | """What others call this droid""" 41 | name: String! 42 | 43 | """This droid's primary function""" 44 | primaryFunction: String 45 | } 46 | 47 | """The episodes in the Star Wars trilogy""" 48 | enum Episode { 49 | """Star Wars Episode IV: A New Hope, released in 1977.""" 50 | NEWHOPE 51 | 52 | """Star Wars Episode V: The Empire Strikes Back, released in 1980.""" 53 | EMPIRE 54 | 55 | """Star Wars Episode VI: Return of the Jedi, released in 1983.""" 56 | JEDI 57 | } 58 | 59 | """A connection object for a character's friends""" 60 | type FriendsConnection { 61 | """The edges for each of the character's friends.""" 62 | edges: [FriendsEdge] 63 | 64 | """A list of the friends, as a convenience when edges are not needed.""" 65 | friends: [Character] 66 | 67 | """Information for paginating this connection""" 68 | pageInfo: PageInfo! 69 | 70 | """The total number of friends""" 71 | totalCount: Int 72 | } 73 | 74 | """An edge object for a character's friends""" 75 | type FriendsEdge { 76 | """A cursor used for pagination""" 77 | cursor: ID! 78 | 79 | """The character represented by this friendship edge""" 80 | node: Character 81 | } 82 | 83 | """A humanoid creature from the Star Wars universe""" 84 | type Human implements Character { 85 | """The movies this human appears in""" 86 | appearsIn: [Episode]! 87 | 88 | """This human's friends, or an empty list if they have none""" 89 | friends: [Character] 90 | 91 | """The friends of the human exposed as a connection with edges""" 92 | friendsConnection(first: Int, after: ID): FriendsConnection! 93 | 94 | """Height in the preferred unit, default is meters""" 95 | height(unit: LengthUnit = METER): Float 96 | 97 | """The home planet of the human, or null if unknown""" 98 | homePlanet: String 99 | 100 | """The ID of the human""" 101 | id: ID! 102 | 103 | """Mass in kilograms, or null if unknown""" 104 | mass: Float 105 | 106 | """What this human calls themselves""" 107 | name: String! 108 | 109 | """A list of starships this person has piloted, or an empty list if none""" 110 | starships: [Starship] 111 | } 112 | 113 | """Units of height""" 114 | enum LengthUnit { 115 | """The standard unit around the world""" 116 | METER 117 | 118 | """Primarily used in the United States""" 119 | FOOT 120 | } 121 | 122 | """The mutation type, represents all updates we can make to our data""" 123 | type Mutation { 124 | createReview(episode: Episode, review: ReviewInput!): Review 125 | } 126 | 127 | """Information for paginating this connection""" 128 | type PageInfo { 129 | endCursor: ID 130 | hasNextPage: Boolean! 131 | startCursor: ID 132 | } 133 | 134 | """ 135 | The query type, represents all of the entry points into our object graph 136 | """ 137 | type Query { 138 | character(id: ID!): Character 139 | droid(id: ID!): Droid 140 | hero(episode: Episode): Character 141 | human(id: ID!): Human 142 | reviews(episode: Episode!): [Review] 143 | search(text: String): [SearchResult] 144 | starship(id: ID!): Starship 145 | } 146 | 147 | """Represents a review for a movie""" 148 | type Review { 149 | """Comment about the movie""" 150 | commentary: String 151 | 152 | """The number of stars this review gave, 1-5""" 153 | stars: Int! 154 | } 155 | 156 | """The input object sent when someone is creating a new review""" 157 | input ReviewInput { 158 | """0-5 stars""" 159 | stars: Int! 160 | 161 | """Comment about the movie, optional""" 162 | commentary: String 163 | 164 | """Favorite color, optional""" 165 | favorite_color: ColorInput 166 | } 167 | 168 | union SearchResult = Human | Droid | Starship 169 | 170 | type Starship { 171 | coordinates: [[Float!]!] 172 | 173 | """The ID of the starship""" 174 | id: ID! 175 | 176 | """Length of the starship, along the longest axis""" 177 | length(unit: LengthUnit = METER): Float 178 | 179 | """The name of the starship""" 180 | name: String! 181 | } 182 | -------------------------------------------------------------------------------- /__fixtures__/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": { 8 | "name": "Mutation" 9 | }, 10 | "subscriptionType": null, 11 | "types": [ 12 | { 13 | "kind": "OBJECT", 14 | "name": "Query", 15 | "description": "The query type, represents all of the entry points into our object graph", 16 | "fields": [ 17 | { 18 | "name": "hero", 19 | "description": "", 20 | "args": [ 21 | { 22 | "name": "episode", 23 | "description": "", 24 | "type": { 25 | "kind": "ENUM", 26 | "name": "Episode", 27 | "ofType": null 28 | }, 29 | "defaultValue": null 30 | } 31 | ], 32 | "type": { 33 | "kind": "INTERFACE", 34 | "name": "Character", 35 | "ofType": null 36 | }, 37 | "isDeprecated": false, 38 | "deprecationReason": null 39 | }, 40 | { 41 | "name": "reviews", 42 | "description": "", 43 | "args": [ 44 | { 45 | "name": "episode", 46 | "description": "", 47 | "type": { 48 | "kind": "NON_NULL", 49 | "name": null, 50 | "ofType": { 51 | "kind": "ENUM", 52 | "name": "Episode", 53 | "ofType": null 54 | } 55 | }, 56 | "defaultValue": null 57 | } 58 | ], 59 | "type": { 60 | "kind": "LIST", 61 | "name": null, 62 | "ofType": { 63 | "kind": "OBJECT", 64 | "name": "Review", 65 | "ofType": null 66 | } 67 | }, 68 | "isDeprecated": false, 69 | "deprecationReason": null 70 | }, 71 | { 72 | "name": "search", 73 | "description": "", 74 | "args": [ 75 | { 76 | "name": "text", 77 | "description": "", 78 | "type": { 79 | "kind": "SCALAR", 80 | "name": "String", 81 | "ofType": null 82 | }, 83 | "defaultValue": null 84 | } 85 | ], 86 | "type": { 87 | "kind": "LIST", 88 | "name": null, 89 | "ofType": { 90 | "kind": "UNION", 91 | "name": "SearchResult", 92 | "ofType": null 93 | } 94 | }, 95 | "isDeprecated": false, 96 | "deprecationReason": null 97 | }, 98 | { 99 | "name": "character", 100 | "description": "", 101 | "args": [ 102 | { 103 | "name": "id", 104 | "description": "", 105 | "type": { 106 | "kind": "NON_NULL", 107 | "name": null, 108 | "ofType": { 109 | "kind": "SCALAR", 110 | "name": "ID", 111 | "ofType": null 112 | } 113 | }, 114 | "defaultValue": null 115 | } 116 | ], 117 | "type": { 118 | "kind": "INTERFACE", 119 | "name": "Character", 120 | "ofType": null 121 | }, 122 | "isDeprecated": false, 123 | "deprecationReason": null 124 | }, 125 | { 126 | "name": "droid", 127 | "description": "", 128 | "args": [ 129 | { 130 | "name": "id", 131 | "description": "", 132 | "type": { 133 | "kind": "NON_NULL", 134 | "name": null, 135 | "ofType": { 136 | "kind": "SCALAR", 137 | "name": "ID", 138 | "ofType": null 139 | } 140 | }, 141 | "defaultValue": null 142 | } 143 | ], 144 | "type": { 145 | "kind": "OBJECT", 146 | "name": "Droid", 147 | "ofType": null 148 | }, 149 | "isDeprecated": false, 150 | "deprecationReason": null 151 | }, 152 | { 153 | "name": "human", 154 | "description": "", 155 | "args": [ 156 | { 157 | "name": "id", 158 | "description": "", 159 | "type": { 160 | "kind": "NON_NULL", 161 | "name": null, 162 | "ofType": { 163 | "kind": "SCALAR", 164 | "name": "ID", 165 | "ofType": null 166 | } 167 | }, 168 | "defaultValue": null 169 | } 170 | ], 171 | "type": { 172 | "kind": "OBJECT", 173 | "name": "Human", 174 | "ofType": null 175 | }, 176 | "isDeprecated": false, 177 | "deprecationReason": null 178 | }, 179 | { 180 | "name": "starship", 181 | "description": "", 182 | "args": [ 183 | { 184 | "name": "id", 185 | "description": "", 186 | "type": { 187 | "kind": "NON_NULL", 188 | "name": null, 189 | "ofType": { 190 | "kind": "SCALAR", 191 | "name": "ID", 192 | "ofType": null 193 | } 194 | }, 195 | "defaultValue": null 196 | } 197 | ], 198 | "type": { 199 | "kind": "OBJECT", 200 | "name": "Starship", 201 | "ofType": null 202 | }, 203 | "isDeprecated": false, 204 | "deprecationReason": null 205 | } 206 | ], 207 | "inputFields": null, 208 | "interfaces": [], 209 | "enumValues": null, 210 | "possibleTypes": null 211 | }, 212 | { 213 | "kind": "ENUM", 214 | "name": "Episode", 215 | "description": "The episodes in the Star Wars trilogy", 216 | "fields": null, 217 | "inputFields": null, 218 | "interfaces": null, 219 | "enumValues": [ 220 | { 221 | "name": "NEWHOPE", 222 | "description": "Star Wars Episode IV: A New Hope, released in 1977.", 223 | "isDeprecated": false, 224 | "deprecationReason": null 225 | }, 226 | { 227 | "name": "EMPIRE", 228 | "description": "Star Wars Episode V: The Empire Strikes Back, released in 1980.", 229 | "isDeprecated": false, 230 | "deprecationReason": null 231 | }, 232 | { 233 | "name": "JEDI", 234 | "description": "Star Wars Episode VI: Return of the Jedi, released in 1983.", 235 | "isDeprecated": false, 236 | "deprecationReason": null 237 | } 238 | ], 239 | "possibleTypes": null 240 | }, 241 | { 242 | "kind": "INTERFACE", 243 | "name": "Character", 244 | "description": "A character from the Star Wars universe", 245 | "fields": [ 246 | { 247 | "name": "id", 248 | "description": "The ID of the character", 249 | "args": [], 250 | "type": { 251 | "kind": "NON_NULL", 252 | "name": null, 253 | "ofType": { 254 | "kind": "SCALAR", 255 | "name": "ID", 256 | "ofType": null 257 | } 258 | }, 259 | "isDeprecated": false, 260 | "deprecationReason": null 261 | }, 262 | { 263 | "name": "name", 264 | "description": "The name of the character", 265 | "args": [], 266 | "type": { 267 | "kind": "NON_NULL", 268 | "name": null, 269 | "ofType": { 270 | "kind": "SCALAR", 271 | "name": "String", 272 | "ofType": null 273 | } 274 | }, 275 | "isDeprecated": false, 276 | "deprecationReason": null 277 | }, 278 | { 279 | "name": "friends", 280 | "description": "The friends of the character, or an empty list if they have none", 281 | "args": [], 282 | "type": { 283 | "kind": "LIST", 284 | "name": null, 285 | "ofType": { 286 | "kind": "INTERFACE", 287 | "name": "Character", 288 | "ofType": null 289 | } 290 | }, 291 | "isDeprecated": false, 292 | "deprecationReason": null 293 | }, 294 | { 295 | "name": "friendsConnection", 296 | "description": "The friends of the character exposed as a connection with edges", 297 | "args": [ 298 | { 299 | "name": "first", 300 | "description": "", 301 | "type": { 302 | "kind": "SCALAR", 303 | "name": "Int", 304 | "ofType": null 305 | }, 306 | "defaultValue": null 307 | }, 308 | { 309 | "name": "after", 310 | "description": "", 311 | "type": { 312 | "kind": "SCALAR", 313 | "name": "ID", 314 | "ofType": null 315 | }, 316 | "defaultValue": null 317 | } 318 | ], 319 | "type": { 320 | "kind": "NON_NULL", 321 | "name": null, 322 | "ofType": { 323 | "kind": "OBJECT", 324 | "name": "FriendsConnection", 325 | "ofType": null 326 | } 327 | }, 328 | "isDeprecated": false, 329 | "deprecationReason": null 330 | }, 331 | { 332 | "name": "appearsIn", 333 | "description": "The movies this character appears in", 334 | "args": [], 335 | "type": { 336 | "kind": "NON_NULL", 337 | "name": null, 338 | "ofType": { 339 | "kind": "LIST", 340 | "name": null, 341 | "ofType": { 342 | "kind": "ENUM", 343 | "name": "Episode", 344 | "ofType": null 345 | } 346 | } 347 | }, 348 | "isDeprecated": false, 349 | "deprecationReason": null 350 | } 351 | ], 352 | "inputFields": null, 353 | "interfaces": null, 354 | "enumValues": null, 355 | "possibleTypes": [ 356 | { 357 | "kind": "OBJECT", 358 | "name": "Human", 359 | "ofType": null 360 | }, 361 | { 362 | "kind": "OBJECT", 363 | "name": "Droid", 364 | "ofType": null 365 | } 366 | ] 367 | }, 368 | { 369 | "kind": "SCALAR", 370 | "name": "ID", 371 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 372 | "fields": null, 373 | "inputFields": null, 374 | "interfaces": null, 375 | "enumValues": null, 376 | "possibleTypes": null 377 | }, 378 | { 379 | "kind": "SCALAR", 380 | "name": "String", 381 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 382 | "fields": null, 383 | "inputFields": null, 384 | "interfaces": null, 385 | "enumValues": null, 386 | "possibleTypes": null 387 | }, 388 | { 389 | "kind": "SCALAR", 390 | "name": "Int", 391 | "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", 392 | "fields": null, 393 | "inputFields": null, 394 | "interfaces": null, 395 | "enumValues": null, 396 | "possibleTypes": null 397 | }, 398 | { 399 | "kind": "OBJECT", 400 | "name": "FriendsConnection", 401 | "description": "A connection object for a character's friends", 402 | "fields": [ 403 | { 404 | "name": "totalCount", 405 | "description": "The total number of friends", 406 | "args": [], 407 | "type": { 408 | "kind": "SCALAR", 409 | "name": "Int", 410 | "ofType": null 411 | }, 412 | "isDeprecated": false, 413 | "deprecationReason": null 414 | }, 415 | { 416 | "name": "edges", 417 | "description": "The edges for each of the character's friends.", 418 | "args": [], 419 | "type": { 420 | "kind": "LIST", 421 | "name": null, 422 | "ofType": { 423 | "kind": "OBJECT", 424 | "name": "FriendsEdge", 425 | "ofType": null 426 | } 427 | }, 428 | "isDeprecated": false, 429 | "deprecationReason": null 430 | }, 431 | { 432 | "name": "friends", 433 | "description": "A list of the friends, as a convenience when edges are not needed.", 434 | "args": [], 435 | "type": { 436 | "kind": "LIST", 437 | "name": null, 438 | "ofType": { 439 | "kind": "INTERFACE", 440 | "name": "Character", 441 | "ofType": null 442 | } 443 | }, 444 | "isDeprecated": false, 445 | "deprecationReason": null 446 | }, 447 | { 448 | "name": "pageInfo", 449 | "description": "Information for paginating this connection", 450 | "args": [], 451 | "type": { 452 | "kind": "NON_NULL", 453 | "name": null, 454 | "ofType": { 455 | "kind": "OBJECT", 456 | "name": "PageInfo", 457 | "ofType": null 458 | } 459 | }, 460 | "isDeprecated": false, 461 | "deprecationReason": null 462 | } 463 | ], 464 | "inputFields": null, 465 | "interfaces": [], 466 | "enumValues": null, 467 | "possibleTypes": null 468 | }, 469 | { 470 | "kind": "OBJECT", 471 | "name": "FriendsEdge", 472 | "description": "An edge object for a character's friends", 473 | "fields": [ 474 | { 475 | "name": "cursor", 476 | "description": "A cursor used for pagination", 477 | "args": [], 478 | "type": { 479 | "kind": "NON_NULL", 480 | "name": null, 481 | "ofType": { 482 | "kind": "SCALAR", 483 | "name": "ID", 484 | "ofType": null 485 | } 486 | }, 487 | "isDeprecated": false, 488 | "deprecationReason": null 489 | }, 490 | { 491 | "name": "node", 492 | "description": "The character represented by this friendship edge", 493 | "args": [], 494 | "type": { 495 | "kind": "INTERFACE", 496 | "name": "Character", 497 | "ofType": null 498 | }, 499 | "isDeprecated": false, 500 | "deprecationReason": null 501 | } 502 | ], 503 | "inputFields": null, 504 | "interfaces": [], 505 | "enumValues": null, 506 | "possibleTypes": null 507 | }, 508 | { 509 | "kind": "OBJECT", 510 | "name": "PageInfo", 511 | "description": "Information for paginating this connection", 512 | "fields": [ 513 | { 514 | "name": "startCursor", 515 | "description": "", 516 | "args": [], 517 | "type": { 518 | "kind": "SCALAR", 519 | "name": "ID", 520 | "ofType": null 521 | }, 522 | "isDeprecated": false, 523 | "deprecationReason": null 524 | }, 525 | { 526 | "name": "endCursor", 527 | "description": "", 528 | "args": [], 529 | "type": { 530 | "kind": "SCALAR", 531 | "name": "ID", 532 | "ofType": null 533 | }, 534 | "isDeprecated": false, 535 | "deprecationReason": null 536 | }, 537 | { 538 | "name": "hasNextPage", 539 | "description": "", 540 | "args": [], 541 | "type": { 542 | "kind": "NON_NULL", 543 | "name": null, 544 | "ofType": { 545 | "kind": "SCALAR", 546 | "name": "Boolean", 547 | "ofType": null 548 | } 549 | }, 550 | "isDeprecated": false, 551 | "deprecationReason": null 552 | } 553 | ], 554 | "inputFields": null, 555 | "interfaces": [], 556 | "enumValues": null, 557 | "possibleTypes": null 558 | }, 559 | { 560 | "kind": "SCALAR", 561 | "name": "Boolean", 562 | "description": "The `Boolean` scalar type represents `true` or `false`.", 563 | "fields": null, 564 | "inputFields": null, 565 | "interfaces": null, 566 | "enumValues": null, 567 | "possibleTypes": null 568 | }, 569 | { 570 | "kind": "OBJECT", 571 | "name": "Review", 572 | "description": "Represents a review for a movie", 573 | "fields": [ 574 | { 575 | "name": "stars", 576 | "description": "The number of stars this review gave, 1-5", 577 | "args": [], 578 | "type": { 579 | "kind": "NON_NULL", 580 | "name": null, 581 | "ofType": { 582 | "kind": "SCALAR", 583 | "name": "Int", 584 | "ofType": null 585 | } 586 | }, 587 | "isDeprecated": false, 588 | "deprecationReason": null 589 | }, 590 | { 591 | "name": "commentary", 592 | "description": "Comment about the movie", 593 | "args": [], 594 | "type": { 595 | "kind": "SCALAR", 596 | "name": "String", 597 | "ofType": null 598 | }, 599 | "isDeprecated": false, 600 | "deprecationReason": null 601 | } 602 | ], 603 | "inputFields": null, 604 | "interfaces": [], 605 | "enumValues": null, 606 | "possibleTypes": null 607 | }, 608 | { 609 | "kind": "UNION", 610 | "name": "SearchResult", 611 | "description": "", 612 | "fields": null, 613 | "inputFields": null, 614 | "interfaces": null, 615 | "enumValues": null, 616 | "possibleTypes": [ 617 | { 618 | "kind": "OBJECT", 619 | "name": "Human", 620 | "ofType": null 621 | }, 622 | { 623 | "kind": "OBJECT", 624 | "name": "Droid", 625 | "ofType": null 626 | }, 627 | { 628 | "kind": "OBJECT", 629 | "name": "Starship", 630 | "ofType": null 631 | } 632 | ] 633 | }, 634 | { 635 | "kind": "OBJECT", 636 | "name": "Human", 637 | "description": "A humanoid creature from the Star Wars universe", 638 | "fields": [ 639 | { 640 | "name": "id", 641 | "description": "The ID of the human", 642 | "args": [], 643 | "type": { 644 | "kind": "NON_NULL", 645 | "name": null, 646 | "ofType": { 647 | "kind": "SCALAR", 648 | "name": "ID", 649 | "ofType": null 650 | } 651 | }, 652 | "isDeprecated": false, 653 | "deprecationReason": null 654 | }, 655 | { 656 | "name": "name", 657 | "description": "What this human calls themselves", 658 | "args": [], 659 | "type": { 660 | "kind": "NON_NULL", 661 | "name": null, 662 | "ofType": { 663 | "kind": "SCALAR", 664 | "name": "String", 665 | "ofType": null 666 | } 667 | }, 668 | "isDeprecated": false, 669 | "deprecationReason": null 670 | }, 671 | { 672 | "name": "homePlanet", 673 | "description": "The home planet of the human, or null if unknown", 674 | "args": [], 675 | "type": { 676 | "kind": "SCALAR", 677 | "name": "String", 678 | "ofType": null 679 | }, 680 | "isDeprecated": false, 681 | "deprecationReason": null 682 | }, 683 | { 684 | "name": "height", 685 | "description": "Height in the preferred unit, default is meters", 686 | "args": [ 687 | { 688 | "name": "unit", 689 | "description": "", 690 | "type": { 691 | "kind": "ENUM", 692 | "name": "LengthUnit", 693 | "ofType": null 694 | }, 695 | "defaultValue": "METER" 696 | } 697 | ], 698 | "type": { 699 | "kind": "SCALAR", 700 | "name": "Float", 701 | "ofType": null 702 | }, 703 | "isDeprecated": false, 704 | "deprecationReason": null 705 | }, 706 | { 707 | "name": "mass", 708 | "description": "Mass in kilograms, or null if unknown", 709 | "args": [], 710 | "type": { 711 | "kind": "SCALAR", 712 | "name": "Float", 713 | "ofType": null 714 | }, 715 | "isDeprecated": false, 716 | "deprecationReason": null 717 | }, 718 | { 719 | "name": "friends", 720 | "description": "This human's friends, or an empty list if they have none", 721 | "args": [], 722 | "type": { 723 | "kind": "LIST", 724 | "name": null, 725 | "ofType": { 726 | "kind": "INTERFACE", 727 | "name": "Character", 728 | "ofType": null 729 | } 730 | }, 731 | "isDeprecated": false, 732 | "deprecationReason": null 733 | }, 734 | { 735 | "name": "friendsConnection", 736 | "description": "The friends of the human exposed as a connection with edges", 737 | "args": [ 738 | { 739 | "name": "first", 740 | "description": "", 741 | "type": { 742 | "kind": "SCALAR", 743 | "name": "Int", 744 | "ofType": null 745 | }, 746 | "defaultValue": null 747 | }, 748 | { 749 | "name": "after", 750 | "description": "", 751 | "type": { 752 | "kind": "SCALAR", 753 | "name": "ID", 754 | "ofType": null 755 | }, 756 | "defaultValue": null 757 | } 758 | ], 759 | "type": { 760 | "kind": "NON_NULL", 761 | "name": null, 762 | "ofType": { 763 | "kind": "OBJECT", 764 | "name": "FriendsConnection", 765 | "ofType": null 766 | } 767 | }, 768 | "isDeprecated": false, 769 | "deprecationReason": null 770 | }, 771 | { 772 | "name": "appearsIn", 773 | "description": "The movies this human appears in", 774 | "args": [], 775 | "type": { 776 | "kind": "NON_NULL", 777 | "name": null, 778 | "ofType": { 779 | "kind": "LIST", 780 | "name": null, 781 | "ofType": { 782 | "kind": "ENUM", 783 | "name": "Episode", 784 | "ofType": null 785 | } 786 | } 787 | }, 788 | "isDeprecated": false, 789 | "deprecationReason": null 790 | }, 791 | { 792 | "name": "starships", 793 | "description": "A list of starships this person has piloted, or an empty list if none", 794 | "args": [], 795 | "type": { 796 | "kind": "LIST", 797 | "name": null, 798 | "ofType": { 799 | "kind": "OBJECT", 800 | "name": "Starship", 801 | "ofType": null 802 | } 803 | }, 804 | "isDeprecated": false, 805 | "deprecationReason": null 806 | } 807 | ], 808 | "inputFields": null, 809 | "interfaces": [ 810 | { 811 | "kind": "INTERFACE", 812 | "name": "Character", 813 | "ofType": null 814 | } 815 | ], 816 | "enumValues": null, 817 | "possibleTypes": null 818 | }, 819 | { 820 | "kind": "ENUM", 821 | "name": "LengthUnit", 822 | "description": "Units of height", 823 | "fields": null, 824 | "inputFields": null, 825 | "interfaces": null, 826 | "enumValues": [ 827 | { 828 | "name": "METER", 829 | "description": "The standard unit around the world", 830 | "isDeprecated": false, 831 | "deprecationReason": null 832 | }, 833 | { 834 | "name": "FOOT", 835 | "description": "Primarily used in the United States", 836 | "isDeprecated": false, 837 | "deprecationReason": null 838 | } 839 | ], 840 | "possibleTypes": null 841 | }, 842 | { 843 | "kind": "SCALAR", 844 | "name": "Float", 845 | "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", 846 | "fields": null, 847 | "inputFields": null, 848 | "interfaces": null, 849 | "enumValues": null, 850 | "possibleTypes": null 851 | }, 852 | { 853 | "kind": "OBJECT", 854 | "name": "Starship", 855 | "description": "", 856 | "fields": [ 857 | { 858 | "name": "id", 859 | "description": "The ID of the starship", 860 | "args": [], 861 | "type": { 862 | "kind": "NON_NULL", 863 | "name": null, 864 | "ofType": { 865 | "kind": "SCALAR", 866 | "name": "ID", 867 | "ofType": null 868 | } 869 | }, 870 | "isDeprecated": false, 871 | "deprecationReason": null 872 | }, 873 | { 874 | "name": "name", 875 | "description": "The name of the starship", 876 | "args": [], 877 | "type": { 878 | "kind": "NON_NULL", 879 | "name": null, 880 | "ofType": { 881 | "kind": "SCALAR", 882 | "name": "String", 883 | "ofType": null 884 | } 885 | }, 886 | "isDeprecated": false, 887 | "deprecationReason": null 888 | }, 889 | { 890 | "name": "length", 891 | "description": "Length of the starship, along the longest axis", 892 | "args": [ 893 | { 894 | "name": "unit", 895 | "description": "", 896 | "type": { 897 | "kind": "ENUM", 898 | "name": "LengthUnit", 899 | "ofType": null 900 | }, 901 | "defaultValue": "METER" 902 | } 903 | ], 904 | "type": { 905 | "kind": "SCALAR", 906 | "name": "Float", 907 | "ofType": null 908 | }, 909 | "isDeprecated": false, 910 | "deprecationReason": null 911 | }, 912 | { 913 | "name": "coordinates", 914 | "description": "", 915 | "args": [], 916 | "type": { 917 | "kind": "LIST", 918 | "name": null, 919 | "ofType": { 920 | "kind": "NON_NULL", 921 | "name": null, 922 | "ofType": { 923 | "kind": "LIST", 924 | "name": null, 925 | "ofType": { 926 | "kind": "NON_NULL", 927 | "name": null, 928 | "ofType": { 929 | "kind": "SCALAR", 930 | "name": "Float", 931 | "ofType": null 932 | } 933 | } 934 | } 935 | } 936 | }, 937 | "isDeprecated": false, 938 | "deprecationReason": null 939 | } 940 | ], 941 | "inputFields": null, 942 | "interfaces": [], 943 | "enumValues": null, 944 | "possibleTypes": null 945 | }, 946 | { 947 | "kind": "OBJECT", 948 | "name": "Droid", 949 | "description": "An autonomous mechanical character in the Star Wars universe", 950 | "fields": [ 951 | { 952 | "name": "id", 953 | "description": "The ID of the droid", 954 | "args": [], 955 | "type": { 956 | "kind": "NON_NULL", 957 | "name": null, 958 | "ofType": { 959 | "kind": "SCALAR", 960 | "name": "ID", 961 | "ofType": null 962 | } 963 | }, 964 | "isDeprecated": false, 965 | "deprecationReason": null 966 | }, 967 | { 968 | "name": "name", 969 | "description": "What others call this droid", 970 | "args": [], 971 | "type": { 972 | "kind": "NON_NULL", 973 | "name": null, 974 | "ofType": { 975 | "kind": "SCALAR", 976 | "name": "String", 977 | "ofType": null 978 | } 979 | }, 980 | "isDeprecated": false, 981 | "deprecationReason": null 982 | }, 983 | { 984 | "name": "friends", 985 | "description": "This droid's friends, or an empty list if they have none", 986 | "args": [], 987 | "type": { 988 | "kind": "LIST", 989 | "name": null, 990 | "ofType": { 991 | "kind": "INTERFACE", 992 | "name": "Character", 993 | "ofType": null 994 | } 995 | }, 996 | "isDeprecated": false, 997 | "deprecationReason": null 998 | }, 999 | { 1000 | "name": "friendsConnection", 1001 | "description": "The friends of the droid exposed as a connection with edges", 1002 | "args": [ 1003 | { 1004 | "name": "first", 1005 | "description": "", 1006 | "type": { 1007 | "kind": "SCALAR", 1008 | "name": "Int", 1009 | "ofType": null 1010 | }, 1011 | "defaultValue": null 1012 | }, 1013 | { 1014 | "name": "after", 1015 | "description": "", 1016 | "type": { 1017 | "kind": "SCALAR", 1018 | "name": "ID", 1019 | "ofType": null 1020 | }, 1021 | "defaultValue": null 1022 | } 1023 | ], 1024 | "type": { 1025 | "kind": "NON_NULL", 1026 | "name": null, 1027 | "ofType": { 1028 | "kind": "OBJECT", 1029 | "name": "FriendsConnection", 1030 | "ofType": null 1031 | } 1032 | }, 1033 | "isDeprecated": false, 1034 | "deprecationReason": null 1035 | }, 1036 | { 1037 | "name": "appearsIn", 1038 | "description": "The movies this droid appears in", 1039 | "args": [], 1040 | "type": { 1041 | "kind": "NON_NULL", 1042 | "name": null, 1043 | "ofType": { 1044 | "kind": "LIST", 1045 | "name": null, 1046 | "ofType": { 1047 | "kind": "ENUM", 1048 | "name": "Episode", 1049 | "ofType": null 1050 | } 1051 | } 1052 | }, 1053 | "isDeprecated": false, 1054 | "deprecationReason": null 1055 | }, 1056 | { 1057 | "name": "primaryFunction", 1058 | "description": "This droid's primary function", 1059 | "args": [], 1060 | "type": { 1061 | "kind": "SCALAR", 1062 | "name": "String", 1063 | "ofType": null 1064 | }, 1065 | "isDeprecated": false, 1066 | "deprecationReason": null 1067 | } 1068 | ], 1069 | "inputFields": null, 1070 | "interfaces": [ 1071 | { 1072 | "kind": "INTERFACE", 1073 | "name": "Character", 1074 | "ofType": null 1075 | } 1076 | ], 1077 | "enumValues": null, 1078 | "possibleTypes": null 1079 | }, 1080 | { 1081 | "kind": "OBJECT", 1082 | "name": "Mutation", 1083 | "description": "The mutation type, represents all updates we can make to our data", 1084 | "fields": [ 1085 | { 1086 | "name": "createReview", 1087 | "description": "", 1088 | "args": [ 1089 | { 1090 | "name": "episode", 1091 | "description": "", 1092 | "type": { 1093 | "kind": "ENUM", 1094 | "name": "Episode", 1095 | "ofType": null 1096 | }, 1097 | "defaultValue": null 1098 | }, 1099 | { 1100 | "name": "review", 1101 | "description": "", 1102 | "type": { 1103 | "kind": "NON_NULL", 1104 | "name": null, 1105 | "ofType": { 1106 | "kind": "INPUT_OBJECT", 1107 | "name": "ReviewInput", 1108 | "ofType": null 1109 | } 1110 | }, 1111 | "defaultValue": null 1112 | } 1113 | ], 1114 | "type": { 1115 | "kind": "OBJECT", 1116 | "name": "Review", 1117 | "ofType": null 1118 | }, 1119 | "isDeprecated": false, 1120 | "deprecationReason": null 1121 | } 1122 | ], 1123 | "inputFields": null, 1124 | "interfaces": [], 1125 | "enumValues": null, 1126 | "possibleTypes": null 1127 | }, 1128 | { 1129 | "kind": "INPUT_OBJECT", 1130 | "name": "ReviewInput", 1131 | "description": "The input object sent when someone is creating a new review", 1132 | "fields": null, 1133 | "inputFields": [ 1134 | { 1135 | "name": "stars", 1136 | "description": "0-5 stars", 1137 | "type": { 1138 | "kind": "NON_NULL", 1139 | "name": null, 1140 | "ofType": { 1141 | "kind": "SCALAR", 1142 | "name": "Int", 1143 | "ofType": null 1144 | } 1145 | }, 1146 | "defaultValue": null 1147 | }, 1148 | { 1149 | "name": "commentary", 1150 | "description": "Comment about the movie, optional", 1151 | "type": { 1152 | "kind": "SCALAR", 1153 | "name": "String", 1154 | "ofType": null 1155 | }, 1156 | "defaultValue": null 1157 | }, 1158 | { 1159 | "name": "favorite_color", 1160 | "description": "Favorite color, optional", 1161 | "type": { 1162 | "kind": "INPUT_OBJECT", 1163 | "name": "ColorInput", 1164 | "ofType": null 1165 | }, 1166 | "defaultValue": null 1167 | } 1168 | ], 1169 | "interfaces": null, 1170 | "enumValues": null, 1171 | "possibleTypes": null 1172 | }, 1173 | { 1174 | "kind": "INPUT_OBJECT", 1175 | "name": "ColorInput", 1176 | "description": "The input object sent when passing in a color", 1177 | "fields": null, 1178 | "inputFields": [ 1179 | { 1180 | "name": "red", 1181 | "description": "", 1182 | "type": { 1183 | "kind": "NON_NULL", 1184 | "name": null, 1185 | "ofType": { 1186 | "kind": "SCALAR", 1187 | "name": "Int", 1188 | "ofType": null 1189 | } 1190 | }, 1191 | "defaultValue": null 1192 | }, 1193 | { 1194 | "name": "green", 1195 | "description": "", 1196 | "type": { 1197 | "kind": "NON_NULL", 1198 | "name": null, 1199 | "ofType": { 1200 | "kind": "SCALAR", 1201 | "name": "Int", 1202 | "ofType": null 1203 | } 1204 | }, 1205 | "defaultValue": null 1206 | }, 1207 | { 1208 | "name": "blue", 1209 | "description": "", 1210 | "type": { 1211 | "kind": "NON_NULL", 1212 | "name": null, 1213 | "ofType": { 1214 | "kind": "SCALAR", 1215 | "name": "Int", 1216 | "ofType": null 1217 | } 1218 | }, 1219 | "defaultValue": null 1220 | } 1221 | ], 1222 | "interfaces": null, 1223 | "enumValues": null, 1224 | "possibleTypes": null 1225 | }, 1226 | { 1227 | "kind": "OBJECT", 1228 | "name": "__Schema", 1229 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 1230 | "fields": [ 1231 | { 1232 | "name": "types", 1233 | "description": "A list of all types supported by this server.", 1234 | "args": [], 1235 | "type": { 1236 | "kind": "NON_NULL", 1237 | "name": null, 1238 | "ofType": { 1239 | "kind": "LIST", 1240 | "name": null, 1241 | "ofType": { 1242 | "kind": "NON_NULL", 1243 | "name": null, 1244 | "ofType": { 1245 | "kind": "OBJECT", 1246 | "name": "__Type", 1247 | "ofType": null 1248 | } 1249 | } 1250 | } 1251 | }, 1252 | "isDeprecated": false, 1253 | "deprecationReason": null 1254 | }, 1255 | { 1256 | "name": "queryType", 1257 | "description": "The type that query operations will be rooted at.", 1258 | "args": [], 1259 | "type": { 1260 | "kind": "NON_NULL", 1261 | "name": null, 1262 | "ofType": { 1263 | "kind": "OBJECT", 1264 | "name": "__Type", 1265 | "ofType": null 1266 | } 1267 | }, 1268 | "isDeprecated": false, 1269 | "deprecationReason": null 1270 | }, 1271 | { 1272 | "name": "mutationType", 1273 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 1274 | "args": [], 1275 | "type": { 1276 | "kind": "OBJECT", 1277 | "name": "__Type", 1278 | "ofType": null 1279 | }, 1280 | "isDeprecated": false, 1281 | "deprecationReason": null 1282 | }, 1283 | { 1284 | "name": "subscriptionType", 1285 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 1286 | "args": [], 1287 | "type": { 1288 | "kind": "OBJECT", 1289 | "name": "__Type", 1290 | "ofType": null 1291 | }, 1292 | "isDeprecated": false, 1293 | "deprecationReason": null 1294 | }, 1295 | { 1296 | "name": "directives", 1297 | "description": "A list of all directives supported by this server.", 1298 | "args": [], 1299 | "type": { 1300 | "kind": "NON_NULL", 1301 | "name": null, 1302 | "ofType": { 1303 | "kind": "LIST", 1304 | "name": null, 1305 | "ofType": { 1306 | "kind": "NON_NULL", 1307 | "name": null, 1308 | "ofType": { 1309 | "kind": "OBJECT", 1310 | "name": "__Directive", 1311 | "ofType": null 1312 | } 1313 | } 1314 | } 1315 | }, 1316 | "isDeprecated": false, 1317 | "deprecationReason": null 1318 | } 1319 | ], 1320 | "inputFields": null, 1321 | "interfaces": [], 1322 | "enumValues": null, 1323 | "possibleTypes": null 1324 | }, 1325 | { 1326 | "kind": "OBJECT", 1327 | "name": "__Type", 1328 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 1329 | "fields": [ 1330 | { 1331 | "name": "kind", 1332 | "description": null, 1333 | "args": [], 1334 | "type": { 1335 | "kind": "NON_NULL", 1336 | "name": null, 1337 | "ofType": { 1338 | "kind": "ENUM", 1339 | "name": "__TypeKind", 1340 | "ofType": null 1341 | } 1342 | }, 1343 | "isDeprecated": false, 1344 | "deprecationReason": null 1345 | }, 1346 | { 1347 | "name": "name", 1348 | "description": null, 1349 | "args": [], 1350 | "type": { 1351 | "kind": "SCALAR", 1352 | "name": "String", 1353 | "ofType": null 1354 | }, 1355 | "isDeprecated": false, 1356 | "deprecationReason": null 1357 | }, 1358 | { 1359 | "name": "description", 1360 | "description": null, 1361 | "args": [], 1362 | "type": { 1363 | "kind": "SCALAR", 1364 | "name": "String", 1365 | "ofType": null 1366 | }, 1367 | "isDeprecated": false, 1368 | "deprecationReason": null 1369 | }, 1370 | { 1371 | "name": "fields", 1372 | "description": null, 1373 | "args": [ 1374 | { 1375 | "name": "includeDeprecated", 1376 | "description": null, 1377 | "type": { 1378 | "kind": "SCALAR", 1379 | "name": "Boolean", 1380 | "ofType": null 1381 | }, 1382 | "defaultValue": "false" 1383 | } 1384 | ], 1385 | "type": { 1386 | "kind": "LIST", 1387 | "name": null, 1388 | "ofType": { 1389 | "kind": "NON_NULL", 1390 | "name": null, 1391 | "ofType": { 1392 | "kind": "OBJECT", 1393 | "name": "__Field", 1394 | "ofType": null 1395 | } 1396 | } 1397 | }, 1398 | "isDeprecated": false, 1399 | "deprecationReason": null 1400 | }, 1401 | { 1402 | "name": "interfaces", 1403 | "description": null, 1404 | "args": [], 1405 | "type": { 1406 | "kind": "LIST", 1407 | "name": null, 1408 | "ofType": { 1409 | "kind": "NON_NULL", 1410 | "name": null, 1411 | "ofType": { 1412 | "kind": "OBJECT", 1413 | "name": "__Type", 1414 | "ofType": null 1415 | } 1416 | } 1417 | }, 1418 | "isDeprecated": false, 1419 | "deprecationReason": null 1420 | }, 1421 | { 1422 | "name": "possibleTypes", 1423 | "description": null, 1424 | "args": [], 1425 | "type": { 1426 | "kind": "LIST", 1427 | "name": null, 1428 | "ofType": { 1429 | "kind": "NON_NULL", 1430 | "name": null, 1431 | "ofType": { 1432 | "kind": "OBJECT", 1433 | "name": "__Type", 1434 | "ofType": null 1435 | } 1436 | } 1437 | }, 1438 | "isDeprecated": false, 1439 | "deprecationReason": null 1440 | }, 1441 | { 1442 | "name": "enumValues", 1443 | "description": null, 1444 | "args": [ 1445 | { 1446 | "name": "includeDeprecated", 1447 | "description": null, 1448 | "type": { 1449 | "kind": "SCALAR", 1450 | "name": "Boolean", 1451 | "ofType": null 1452 | }, 1453 | "defaultValue": "false" 1454 | } 1455 | ], 1456 | "type": { 1457 | "kind": "LIST", 1458 | "name": null, 1459 | "ofType": { 1460 | "kind": "NON_NULL", 1461 | "name": null, 1462 | "ofType": { 1463 | "kind": "OBJECT", 1464 | "name": "__EnumValue", 1465 | "ofType": null 1466 | } 1467 | } 1468 | }, 1469 | "isDeprecated": false, 1470 | "deprecationReason": null 1471 | }, 1472 | { 1473 | "name": "inputFields", 1474 | "description": null, 1475 | "args": [], 1476 | "type": { 1477 | "kind": "LIST", 1478 | "name": null, 1479 | "ofType": { 1480 | "kind": "NON_NULL", 1481 | "name": null, 1482 | "ofType": { 1483 | "kind": "OBJECT", 1484 | "name": "__InputValue", 1485 | "ofType": null 1486 | } 1487 | } 1488 | }, 1489 | "isDeprecated": false, 1490 | "deprecationReason": null 1491 | }, 1492 | { 1493 | "name": "ofType", 1494 | "description": null, 1495 | "args": [], 1496 | "type": { 1497 | "kind": "OBJECT", 1498 | "name": "__Type", 1499 | "ofType": null 1500 | }, 1501 | "isDeprecated": false, 1502 | "deprecationReason": null 1503 | } 1504 | ], 1505 | "inputFields": null, 1506 | "interfaces": [], 1507 | "enumValues": null, 1508 | "possibleTypes": null 1509 | }, 1510 | { 1511 | "kind": "ENUM", 1512 | "name": "__TypeKind", 1513 | "description": "An enum describing what kind of type a given `__Type` is.", 1514 | "fields": null, 1515 | "inputFields": null, 1516 | "interfaces": null, 1517 | "enumValues": [ 1518 | { 1519 | "name": "SCALAR", 1520 | "description": "Indicates this type is a scalar.", 1521 | "isDeprecated": false, 1522 | "deprecationReason": null 1523 | }, 1524 | { 1525 | "name": "OBJECT", 1526 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 1527 | "isDeprecated": false, 1528 | "deprecationReason": null 1529 | }, 1530 | { 1531 | "name": "INTERFACE", 1532 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 1533 | "isDeprecated": false, 1534 | "deprecationReason": null 1535 | }, 1536 | { 1537 | "name": "UNION", 1538 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 1539 | "isDeprecated": false, 1540 | "deprecationReason": null 1541 | }, 1542 | { 1543 | "name": "ENUM", 1544 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 1545 | "isDeprecated": false, 1546 | "deprecationReason": null 1547 | }, 1548 | { 1549 | "name": "INPUT_OBJECT", 1550 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 1551 | "isDeprecated": false, 1552 | "deprecationReason": null 1553 | }, 1554 | { 1555 | "name": "LIST", 1556 | "description": "Indicates this type is a list. `ofType` is a valid field.", 1557 | "isDeprecated": false, 1558 | "deprecationReason": null 1559 | }, 1560 | { 1561 | "name": "NON_NULL", 1562 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 1563 | "isDeprecated": false, 1564 | "deprecationReason": null 1565 | } 1566 | ], 1567 | "possibleTypes": null 1568 | }, 1569 | { 1570 | "kind": "OBJECT", 1571 | "name": "__Field", 1572 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 1573 | "fields": [ 1574 | { 1575 | "name": "name", 1576 | "description": null, 1577 | "args": [], 1578 | "type": { 1579 | "kind": "NON_NULL", 1580 | "name": null, 1581 | "ofType": { 1582 | "kind": "SCALAR", 1583 | "name": "String", 1584 | "ofType": null 1585 | } 1586 | }, 1587 | "isDeprecated": false, 1588 | "deprecationReason": null 1589 | }, 1590 | { 1591 | "name": "description", 1592 | "description": null, 1593 | "args": [], 1594 | "type": { 1595 | "kind": "SCALAR", 1596 | "name": "String", 1597 | "ofType": null 1598 | }, 1599 | "isDeprecated": false, 1600 | "deprecationReason": null 1601 | }, 1602 | { 1603 | "name": "args", 1604 | "description": null, 1605 | "args": [], 1606 | "type": { 1607 | "kind": "NON_NULL", 1608 | "name": null, 1609 | "ofType": { 1610 | "kind": "LIST", 1611 | "name": null, 1612 | "ofType": { 1613 | "kind": "NON_NULL", 1614 | "name": null, 1615 | "ofType": { 1616 | "kind": "OBJECT", 1617 | "name": "__InputValue", 1618 | "ofType": null 1619 | } 1620 | } 1621 | } 1622 | }, 1623 | "isDeprecated": false, 1624 | "deprecationReason": null 1625 | }, 1626 | { 1627 | "name": "type", 1628 | "description": null, 1629 | "args": [], 1630 | "type": { 1631 | "kind": "NON_NULL", 1632 | "name": null, 1633 | "ofType": { 1634 | "kind": "OBJECT", 1635 | "name": "__Type", 1636 | "ofType": null 1637 | } 1638 | }, 1639 | "isDeprecated": false, 1640 | "deprecationReason": null 1641 | }, 1642 | { 1643 | "name": "isDeprecated", 1644 | "description": null, 1645 | "args": [], 1646 | "type": { 1647 | "kind": "NON_NULL", 1648 | "name": null, 1649 | "ofType": { 1650 | "kind": "SCALAR", 1651 | "name": "Boolean", 1652 | "ofType": null 1653 | } 1654 | }, 1655 | "isDeprecated": false, 1656 | "deprecationReason": null 1657 | }, 1658 | { 1659 | "name": "deprecationReason", 1660 | "description": null, 1661 | "args": [], 1662 | "type": { 1663 | "kind": "SCALAR", 1664 | "name": "String", 1665 | "ofType": null 1666 | }, 1667 | "isDeprecated": false, 1668 | "deprecationReason": null 1669 | } 1670 | ], 1671 | "inputFields": null, 1672 | "interfaces": [], 1673 | "enumValues": null, 1674 | "possibleTypes": null 1675 | }, 1676 | { 1677 | "kind": "OBJECT", 1678 | "name": "__InputValue", 1679 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 1680 | "fields": [ 1681 | { 1682 | "name": "name", 1683 | "description": null, 1684 | "args": [], 1685 | "type": { 1686 | "kind": "NON_NULL", 1687 | "name": null, 1688 | "ofType": { 1689 | "kind": "SCALAR", 1690 | "name": "String", 1691 | "ofType": null 1692 | } 1693 | }, 1694 | "isDeprecated": false, 1695 | "deprecationReason": null 1696 | }, 1697 | { 1698 | "name": "description", 1699 | "description": null, 1700 | "args": [], 1701 | "type": { 1702 | "kind": "SCALAR", 1703 | "name": "String", 1704 | "ofType": null 1705 | }, 1706 | "isDeprecated": false, 1707 | "deprecationReason": null 1708 | }, 1709 | { 1710 | "name": "type", 1711 | "description": null, 1712 | "args": [], 1713 | "type": { 1714 | "kind": "NON_NULL", 1715 | "name": null, 1716 | "ofType": { 1717 | "kind": "OBJECT", 1718 | "name": "__Type", 1719 | "ofType": null 1720 | } 1721 | }, 1722 | "isDeprecated": false, 1723 | "deprecationReason": null 1724 | }, 1725 | { 1726 | "name": "defaultValue", 1727 | "description": "A GraphQL-formatted string representing the default value for this input value.", 1728 | "args": [], 1729 | "type": { 1730 | "kind": "SCALAR", 1731 | "name": "String", 1732 | "ofType": null 1733 | }, 1734 | "isDeprecated": false, 1735 | "deprecationReason": null 1736 | } 1737 | ], 1738 | "inputFields": null, 1739 | "interfaces": [], 1740 | "enumValues": null, 1741 | "possibleTypes": null 1742 | }, 1743 | { 1744 | "kind": "OBJECT", 1745 | "name": "__EnumValue", 1746 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 1747 | "fields": [ 1748 | { 1749 | "name": "name", 1750 | "description": null, 1751 | "args": [], 1752 | "type": { 1753 | "kind": "NON_NULL", 1754 | "name": null, 1755 | "ofType": { 1756 | "kind": "SCALAR", 1757 | "name": "String", 1758 | "ofType": null 1759 | } 1760 | }, 1761 | "isDeprecated": false, 1762 | "deprecationReason": null 1763 | }, 1764 | { 1765 | "name": "description", 1766 | "description": null, 1767 | "args": [], 1768 | "type": { 1769 | "kind": "SCALAR", 1770 | "name": "String", 1771 | "ofType": null 1772 | }, 1773 | "isDeprecated": false, 1774 | "deprecationReason": null 1775 | }, 1776 | { 1777 | "name": "isDeprecated", 1778 | "description": null, 1779 | "args": [], 1780 | "type": { 1781 | "kind": "NON_NULL", 1782 | "name": null, 1783 | "ofType": { 1784 | "kind": "SCALAR", 1785 | "name": "Boolean", 1786 | "ofType": null 1787 | } 1788 | }, 1789 | "isDeprecated": false, 1790 | "deprecationReason": null 1791 | }, 1792 | { 1793 | "name": "deprecationReason", 1794 | "description": null, 1795 | "args": [], 1796 | "type": { 1797 | "kind": "SCALAR", 1798 | "name": "String", 1799 | "ofType": null 1800 | }, 1801 | "isDeprecated": false, 1802 | "deprecationReason": null 1803 | } 1804 | ], 1805 | "inputFields": null, 1806 | "interfaces": [], 1807 | "enumValues": null, 1808 | "possibleTypes": null 1809 | }, 1810 | { 1811 | "kind": "OBJECT", 1812 | "name": "__Directive", 1813 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 1814 | "fields": [ 1815 | { 1816 | "name": "name", 1817 | "description": null, 1818 | "args": [], 1819 | "type": { 1820 | "kind": "NON_NULL", 1821 | "name": null, 1822 | "ofType": { 1823 | "kind": "SCALAR", 1824 | "name": "String", 1825 | "ofType": null 1826 | } 1827 | }, 1828 | "isDeprecated": false, 1829 | "deprecationReason": null 1830 | }, 1831 | { 1832 | "name": "description", 1833 | "description": null, 1834 | "args": [], 1835 | "type": { 1836 | "kind": "SCALAR", 1837 | "name": "String", 1838 | "ofType": null 1839 | }, 1840 | "isDeprecated": false, 1841 | "deprecationReason": null 1842 | }, 1843 | { 1844 | "name": "locations", 1845 | "description": null, 1846 | "args": [], 1847 | "type": { 1848 | "kind": "NON_NULL", 1849 | "name": null, 1850 | "ofType": { 1851 | "kind": "LIST", 1852 | "name": null, 1853 | "ofType": { 1854 | "kind": "NON_NULL", 1855 | "name": null, 1856 | "ofType": { 1857 | "kind": "ENUM", 1858 | "name": "__DirectiveLocation", 1859 | "ofType": null 1860 | } 1861 | } 1862 | } 1863 | }, 1864 | "isDeprecated": false, 1865 | "deprecationReason": null 1866 | }, 1867 | { 1868 | "name": "args", 1869 | "description": null, 1870 | "args": [], 1871 | "type": { 1872 | "kind": "NON_NULL", 1873 | "name": null, 1874 | "ofType": { 1875 | "kind": "LIST", 1876 | "name": null, 1877 | "ofType": { 1878 | "kind": "NON_NULL", 1879 | "name": null, 1880 | "ofType": { 1881 | "kind": "OBJECT", 1882 | "name": "__InputValue", 1883 | "ofType": null 1884 | } 1885 | } 1886 | } 1887 | }, 1888 | "isDeprecated": false, 1889 | "deprecationReason": null 1890 | }, 1891 | { 1892 | "name": "onOperation", 1893 | "description": null, 1894 | "args": [], 1895 | "type": { 1896 | "kind": "NON_NULL", 1897 | "name": null, 1898 | "ofType": { 1899 | "kind": "SCALAR", 1900 | "name": "Boolean", 1901 | "ofType": null 1902 | } 1903 | }, 1904 | "isDeprecated": true, 1905 | "deprecationReason": "Use `locations`." 1906 | }, 1907 | { 1908 | "name": "onFragment", 1909 | "description": null, 1910 | "args": [], 1911 | "type": { 1912 | "kind": "NON_NULL", 1913 | "name": null, 1914 | "ofType": { 1915 | "kind": "SCALAR", 1916 | "name": "Boolean", 1917 | "ofType": null 1918 | } 1919 | }, 1920 | "isDeprecated": true, 1921 | "deprecationReason": "Use `locations`." 1922 | }, 1923 | { 1924 | "name": "onField", 1925 | "description": null, 1926 | "args": [], 1927 | "type": { 1928 | "kind": "NON_NULL", 1929 | "name": null, 1930 | "ofType": { 1931 | "kind": "SCALAR", 1932 | "name": "Boolean", 1933 | "ofType": null 1934 | } 1935 | }, 1936 | "isDeprecated": true, 1937 | "deprecationReason": "Use `locations`." 1938 | } 1939 | ], 1940 | "inputFields": null, 1941 | "interfaces": [], 1942 | "enumValues": null, 1943 | "possibleTypes": null 1944 | }, 1945 | { 1946 | "kind": "ENUM", 1947 | "name": "__DirectiveLocation", 1948 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 1949 | "fields": null, 1950 | "inputFields": null, 1951 | "interfaces": null, 1952 | "enumValues": [ 1953 | { 1954 | "name": "QUERY", 1955 | "description": "Location adjacent to a query operation.", 1956 | "isDeprecated": false, 1957 | "deprecationReason": null 1958 | }, 1959 | { 1960 | "name": "MUTATION", 1961 | "description": "Location adjacent to a mutation operation.", 1962 | "isDeprecated": false, 1963 | "deprecationReason": null 1964 | }, 1965 | { 1966 | "name": "SUBSCRIPTION", 1967 | "description": "Location adjacent to a subscription operation.", 1968 | "isDeprecated": false, 1969 | "deprecationReason": null 1970 | }, 1971 | { 1972 | "name": "FIELD", 1973 | "description": "Location adjacent to a field.", 1974 | "isDeprecated": false, 1975 | "deprecationReason": null 1976 | }, 1977 | { 1978 | "name": "FRAGMENT_DEFINITION", 1979 | "description": "Location adjacent to a fragment definition.", 1980 | "isDeprecated": false, 1981 | "deprecationReason": null 1982 | }, 1983 | { 1984 | "name": "FRAGMENT_SPREAD", 1985 | "description": "Location adjacent to a fragment spread.", 1986 | "isDeprecated": false, 1987 | "deprecationReason": null 1988 | }, 1989 | { 1990 | "name": "INLINE_FRAGMENT", 1991 | "description": "Location adjacent to an inline fragment.", 1992 | "isDeprecated": false, 1993 | "deprecationReason": null 1994 | }, 1995 | { 1996 | "name": "SCHEMA", 1997 | "description": "Location adjacent to a schema definition.", 1998 | "isDeprecated": false, 1999 | "deprecationReason": null 2000 | }, 2001 | { 2002 | "name": "SCALAR", 2003 | "description": "Location adjacent to a scalar definition.", 2004 | "isDeprecated": false, 2005 | "deprecationReason": null 2006 | }, 2007 | { 2008 | "name": "OBJECT", 2009 | "description": "Location adjacent to an object type definition.", 2010 | "isDeprecated": false, 2011 | "deprecationReason": null 2012 | }, 2013 | { 2014 | "name": "FIELD_DEFINITION", 2015 | "description": "Location adjacent to a field definition.", 2016 | "isDeprecated": false, 2017 | "deprecationReason": null 2018 | }, 2019 | { 2020 | "name": "ARGUMENT_DEFINITION", 2021 | "description": "Location adjacent to an argument definition.", 2022 | "isDeprecated": false, 2023 | "deprecationReason": null 2024 | }, 2025 | { 2026 | "name": "INTERFACE", 2027 | "description": "Location adjacent to an interface definition.", 2028 | "isDeprecated": false, 2029 | "deprecationReason": null 2030 | }, 2031 | { 2032 | "name": "UNION", 2033 | "description": "Location adjacent to a union definition.", 2034 | "isDeprecated": false, 2035 | "deprecationReason": null 2036 | }, 2037 | { 2038 | "name": "ENUM", 2039 | "description": "Location adjacent to an enum definition.", 2040 | "isDeprecated": false, 2041 | "deprecationReason": null 2042 | }, 2043 | { 2044 | "name": "ENUM_VALUE", 2045 | "description": "Location adjacent to an enum value definition.", 2046 | "isDeprecated": false, 2047 | "deprecationReason": null 2048 | }, 2049 | { 2050 | "name": "INPUT_OBJECT", 2051 | "description": "Location adjacent to an input object type definition.", 2052 | "isDeprecated": false, 2053 | "deprecationReason": null 2054 | }, 2055 | { 2056 | "name": "INPUT_FIELD_DEFINITION", 2057 | "description": "Location adjacent to an input object field definition.", 2058 | "isDeprecated": false, 2059 | "deprecationReason": null 2060 | } 2061 | ], 2062 | "possibleTypes": null 2063 | } 2064 | ], 2065 | "directives": [ 2066 | { 2067 | "name": "skip", 2068 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 2069 | "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 2070 | "args": [ 2071 | { 2072 | "name": "if", 2073 | "description": "Skipped when true.", 2074 | "type": { 2075 | "kind": "NON_NULL", 2076 | "name": null, 2077 | "ofType": { 2078 | "kind": "SCALAR", 2079 | "name": "Boolean", 2080 | "ofType": null 2081 | } 2082 | }, 2083 | "defaultValue": null 2084 | } 2085 | ] 2086 | }, 2087 | { 2088 | "name": "include", 2089 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 2090 | "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 2091 | "args": [ 2092 | { 2093 | "name": "if", 2094 | "description": "Included when true.", 2095 | "type": { 2096 | "kind": "NON_NULL", 2097 | "name": null, 2098 | "ofType": { 2099 | "kind": "SCALAR", 2100 | "name": "Boolean", 2101 | "ofType": null 2102 | } 2103 | }, 2104 | "defaultValue": null 2105 | } 2106 | ] 2107 | }, 2108 | { 2109 | "name": "deprecated", 2110 | "description": "Marks an element of a GraphQL schema as no longer supported.", 2111 | "locations": ["FIELD_DEFINITION", "ENUM_VALUE"], 2112 | "args": [ 2113 | { 2114 | "name": "reason", 2115 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", 2116 | "type": { 2117 | "kind": "SCALAR", 2118 | "name": "String", 2119 | "ofType": null 2120 | }, 2121 | "defaultValue": "\"No longer supported\"" 2122 | } 2123 | ] 2124 | } 2125 | ] 2126 | } 2127 | } 2128 | } 2129 | -------------------------------------------------------------------------------- /__fixtures__/testSrc/foo.js: -------------------------------------------------------------------------------- 1 | import { graphql } from "react-relay"; 2 | 3 | const query = graphql` 4 | query findGraphQLTagsQuery { 5 | hero { 6 | id 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const project = path.join(__dirname, '../tsconfig.json') 6 | const dev = fs.existsSync(project) 7 | 8 | if (dev) { 9 | require('ts-node').register({project}) 10 | } 11 | 12 | require(`../${dev ? 'src' : 'lib'}`).run() 13 | .catch(require('@oclif/errors/handle')) 14 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CDThomas/graphql-usage/af28eb521c39b6a36e99e20fa13ec0360407fef5/demo.gif -------------------------------------------------------------------------------- /graphql-usage-ui/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /graphql-usage-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /graphql-usage-ui/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /graphql-usage-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-usage-ui", 3 | "version": "0.3.0", 4 | "proxy": "http://localhost:3001/", 5 | "dependencies": { 6 | "@types/jest": "24.0.13", 7 | "@types/node": "12.0.2", 8 | "@types/react": "16.8.18", 9 | "@types/react-dom": "16.8.4", 10 | "react": "^16.8.6", 11 | "react-dom": "^16.8.6", 12 | "react-scripts": "3.0.1", 13 | "typescript": "3.4.5" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /graphql-usage-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 21 | GraphQL Usage 22 | 23 | 24 | 25 |
26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /graphql-usage-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "GraphQL Usage", 3 | "name": "GraphQL Usage", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import DetailsPanel from "./DetailsPanel"; 4 | import Header from "./Header"; 5 | import { Report, ReportField } from "./reportTypes"; 6 | import Schema from "./Schema"; 7 | 8 | function App() { 9 | const [filter, setFilter] = useState(""); 10 | const handleSearchChange = (searchText: string) => setFilter(searchText); 11 | 12 | const [showDetails, setShowDetails] = useState(false); 13 | const [selectedField, setSelectedField] = useState(null); 14 | const handleFieldClick = (field: ReportField): void => { 15 | setShowDetails(true); 16 | setSelectedField(field); 17 | }; 18 | const handleClose = (): void => { 19 | setShowDetails(false); 20 | }; 21 | 22 | const [report, setReport] = useState(null); 23 | const hasReportLoaded = !!report; 24 | 25 | useEffect(() => { 26 | fetch("/stats") 27 | .then(res => res.json()) 28 | .then((report: Report) => { 29 | setReport(report); 30 | }); 31 | }, [hasReportLoaded]); 32 | 33 | if (!report) { 34 | return
Loading...
; 35 | } 36 | 37 | const types = report.data.types.filter(type => { 38 | const lowerCaseFilter = filter.toLowerCase(); 39 | const typeMatchesFilter = type.name.toLowerCase().includes(lowerCaseFilter); 40 | 41 | const fieldsMatchFilter = type.fields 42 | .map(field => { 43 | const typeMatchesFilter = field.type 44 | .toLowerCase() 45 | .includes(lowerCaseFilter); 46 | const fieldMatchesFilter = field.name 47 | .toLowerCase() 48 | .includes(lowerCaseFilter); 49 | 50 | return fieldMatchesFilter || typeMatchesFilter; 51 | }) 52 | .includes(true); 53 | 54 | return typeMatchesFilter || fieldsMatchFilter; 55 | }); 56 | 57 | return ( 58 |
59 |
60 | {types.length === 0 ? ( 61 |

No types or fields match your search.

62 | ) : ( 63 |
64 | 69 | {showDetails && selectedField && ( 70 | 71 | )} 72 |
73 | )} 74 |
75 | ); 76 | } 77 | 78 | export default App; 79 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/Delimiter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | token: string; 5 | } 6 | 7 | const Delimiter: React.FC = ({ token }) => { 8 | const color = 9 | token === "{" || token === "}" ? "rgba(23,42,58,.5)" : "#555555"; 10 | 11 | return {token}; 12 | }; 13 | 14 | export default Delimiter; 15 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/DetailsPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Delimiter from "./Delimiter"; 4 | import FieldName from "./FieldName"; 5 | import FieldType from "./FieldType"; 6 | import OccurrencesList from "./OccurrencesList"; 7 | import { ReportField } from "./reportTypes"; 8 | import TypeName from "./TypeName"; 9 | 10 | const Panel: React.FC = ({ children }) => { 11 | return ( 12 |
29 | {children} 30 |
31 | ); 32 | }; 33 | 34 | const DetailsPanelHeader: React.FC = ({ children }) => { 35 | return ( 36 |
48 | {children} 49 |
50 | ); 51 | }; 52 | 53 | interface DetailsPanelTitleProps { 54 | field: ReportField; 55 | } 56 | 57 | const DetailsPanelTitle: React.FC = ({ field }) => { 58 | return ( 59 |
60 | {field.parentType} 61 | 62 | {field.name} 63 |
64 | ); 65 | }; 66 | 67 | const DetailsPanelBody: React.FC = ({ children }) => { 68 | return ( 69 |
74 | {children} 75 |
76 | ); 77 | }; 78 | 79 | const DetailsPanelHeading: React.FC = ({ children }) => { 80 | return ( 81 |

91 | {children} 92 |

93 | ); 94 | }; 95 | 96 | interface CloseButtonProps { 97 | onClick(): void; 98 | } 99 | 100 | const CloseButton: React.FC = ({ onClick }) => { 101 | return ( 102 |
111 | ✕ 112 |
113 | ); 114 | }; 115 | 116 | interface Props { 117 | field: ReportField; 118 | onClose(): void; 119 | } 120 | 121 | const DetailsPanel: React.FC = ({ field, onClose }) => { 122 | if (field.occurrences.length === 0) return null; 123 | 124 | return ( 125 | 126 | 127 | 128 | 129 | 130 | 131 | Type 132 | {field.type} 133 | Occurrences 134 | 135 | 136 | 137 | ); 138 | }; 139 | 140 | export default DetailsPanel; 141 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/FieldLine.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Delimiter from "./Delimiter"; 4 | import FieldName from "./FieldName"; 5 | import FieldType from "./FieldType"; 6 | import Label from "./Label"; 7 | import { ReportField } from "./reportTypes"; 8 | 9 | interface Props { 10 | field: ReportField; 11 | filter: string; 12 | onFieldClick(field: ReportField): void; 13 | } 14 | 15 | const FieldLine: React.FC = ({ field, filter, onFieldClick }) => { 16 | const { name, type, occurrences } = field; 17 | const lowerCaseFilter = filter.toLowerCase(); 18 | const fieldMatchesFilter = field.name.toLowerCase().includes(lowerCaseFilter); 19 | const typeMatchesFilter = field.type.toLowerCase().includes(lowerCaseFilter); 20 | const isClickable = field.occurrences.length > 0; 21 | const handleFieldClick = () => { 22 | if (isClickable) onFieldClick(field); 23 | }; 24 | 25 | return ( 26 |
30 |
34 | {name} 35 | {" "} 36 | {type} 37 |
38 | 39 | {occurrences.length === 0 ? ( 40 | 41 | ) : ( 42 | 43 | )} 44 |
45 | ); 46 | }; 47 | 48 | export default FieldLine; 49 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/FieldName.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | highlight?: boolean; 5 | } 6 | 7 | const FieldName: React.FC = ({ children, highlight = false }) => { 8 | const highlightStyles = highlight ? { backgroundColor: "#ffffe0" } : {}; 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default FieldName; 17 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/FieldType.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | highlight?: boolean; 5 | } 6 | 7 | const FieldType: React.FC = ({ children, highlight = false }) => { 8 | const highlightStyles = highlight ? { backgroundColor: "#ffffe0" } : {}; 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default FieldType; 17 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SearchBoxProps { 4 | searchText: string; 5 | onChange(searchText: string): void; 6 | } 7 | 8 | const SearchBox: React.FC = ({ onChange, searchText }) => { 9 | const handleChange = (event: React.ChangeEvent): void => { 10 | onChange(event.target.value); 11 | }; 12 | 13 | return ( 14 | 52 | ); 53 | }; 54 | 55 | interface HeaderProps { 56 | searchText: string; 57 | onSearchChange(searchText: string): void; 58 | } 59 | 60 | const Header: React.FC = ({ searchText, onSearchChange }) => { 61 | return ( 62 |
78 | 79 |
80 | ); 81 | }; 82 | 83 | export default Header; 84 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/Label.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { LIGHT_BLUE, RED, WHITE } from "./constants/colors"; 4 | 5 | type LabelColor = "red" | "blue"; 6 | 7 | interface LabelProps { 8 | color?: LabelColor; 9 | } 10 | 11 | const Label: React.FC = ({ children, color = "blue" }) => { 12 | const hexColor = { 13 | blue: LIGHT_BLUE, 14 | red: RED 15 | }[color]; 16 | 17 | return ( 18 | 34 | {children} 35 | 36 | ); 37 | }; 38 | 39 | export default Label; 40 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/OccurrencesList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ReportField } from "./reportTypes"; 4 | 5 | interface Props { 6 | field: ReportField; 7 | } 8 | 9 | const OccurrencesList: React.FC = ({ field }) => { 10 | return ( 11 |
    17 | {field.occurrences.map(({ filename, rootNodeName }, index) => { 18 | return ( 19 |
  • 25 | 26 | 27 | {rootNodeName} 28 | 29 | 30 |
  • 31 | ); 32 | })} 33 |
34 | ); 35 | }; 36 | 37 | export default OccurrencesList; 38 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/Schema.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ReportField, ReportType } from "./reportTypes"; 4 | import TypeBlock from "./TypeBlock"; 5 | 6 | interface Props { 7 | types: ReportType[]; 8 | filter: string; 9 | onFieldClick(field: ReportField): void; 10 | } 11 | 12 | const Schema: React.FC = ({ types, filter, onFieldClick }) => { 13 | return ( 14 |
22 | {types.map(type => { 23 | return ( 24 | 30 | ); 31 | })} 32 |
33 | ); 34 | }; 35 | 36 | export default Schema; 37 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/TypeBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Delimiter from "./Delimiter"; 4 | import FieldLine from "./FieldLine"; 5 | import { ReportField, ReportType } from "./reportTypes"; 6 | import TypeKind from "./TypeKind"; 7 | import TypeName from "./TypeName"; 8 | 9 | interface Props { 10 | type: ReportType; 11 | filter: string; 12 | onFieldClick(field: ReportField): void; 13 | } 14 | 15 | const TypeBlock: React.FC = ({ type, filter, onFieldClick }) => { 16 | const { name, fields } = type; 17 | const typeMatchesFilter = type.name 18 | .toLowerCase() 19 | .includes(filter.toLowerCase()); 20 | 21 | return ( 22 |
23 |
24 | type{" "} 25 | {name}{" "} 26 | 27 |
28 | {fields.map(field => { 29 | return ( 30 | 36 | ); 37 | })} 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default TypeBlock; 44 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/TypeKind.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TypeKind: React.FC = ({ children }) => { 4 | return {children}; 5 | }; 6 | 7 | export default TypeKind; 8 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/TypeName.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | highlight?: boolean; 5 | } 6 | 7 | const TypeName: React.FC = ({ children, highlight = false }) => { 8 | const highlightStyles = highlight ? { backgroundColor: "#ffffe0" } : {}; 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default TypeName; 18 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/constants/colors.ts: -------------------------------------------------------------------------------- 1 | export const LIGHT_BLUE = "#79d5f1"; 2 | export const RED = "#f25c54"; 3 | export const WHITE = "#ffffff"; 4 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | /* Chrome/Opera/Safari */ 11 | ::-webkit-input-placeholder { 12 | color: "rgba(0, 0, 0, 0.3)"; 13 | } 14 | /* Firefox 19+ */ 15 | ::-moz-placeholder { 16 | color: "rgba(0, 0, 0, 0.3)"; 17 | } 18 | /* IE 10+ */ 19 | :-ms-input-placeholder { 20 | color: "rgba(0, 0, 0, 0.3)"; 21 | } 22 | /* Firefox 18- */ 23 | :-moz-placeholder { 24 | color: "rgba(0, 0, 0, 0.3)"; 25 | } 26 | 27 | code { 28 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 29 | monospace; 30 | } 31 | 32 | .clickable-field-line:hover { 33 | text-decoration: underline; 34 | cursor: pointer; 35 | } 36 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/reportTypes.ts: -------------------------------------------------------------------------------- 1 | // TODO: import these types rather than duplicating them 2 | export interface Report { 3 | data: { 4 | types: ReportType[], 5 | }; 6 | } 7 | 8 | export interface ReportType { 9 | name: string; 10 | fields: ReportField[]; 11 | } 12 | 13 | export interface ReportField { 14 | parentType: string; 15 | type: string; 16 | name: string; 17 | occurrences: ReportOccurrence[]; 18 | } 19 | 20 | export interface ReportOccurrence { 21 | rootNodeName: string; 22 | filename: string; 23 | } 24 | -------------------------------------------------------------------------------- /graphql-usage-ui/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /graphql-usage-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | roots: ["/src"], 4 | transform: { 5 | "^.+\\.tsx?$": "ts-jest" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-usage", 3 | "description": "A tool for refactoring GraphQL APIs", 4 | "version": "0.0.0-semantically-released", 5 | "author": " @CDThomas", 6 | "bin": { 7 | "graphql-usage": "./bin/run" 8 | }, 9 | "bugs": "https://github.com/CDThomas/graphql-usage/issues", 10 | "dependencies": { 11 | "@babel/parser": "^7.4.5", 12 | "@babel/traverse": "^7.5.5", 13 | "@oclif/command": "^1", 14 | "@oclif/config": "^1", 15 | "@oclif/plugin-help": "^2", 16 | "express": "^4.17.1", 17 | "fast-glob": "^3.0.4", 18 | "graphql": "^14.4.2", 19 | "listr": "^0.14.3", 20 | "open": "^6.4.0", 21 | "ramda": "^0.26.1", 22 | "tslib": "^1", 23 | "typescript": "^3.6.3" 24 | }, 25 | "devDependencies": { 26 | "@babel/types": "^7.4.4", 27 | "@oclif/test": "^1.2.4", 28 | "@oclif/tslint": "^3", 29 | "@types/express": "^4.17.1", 30 | "@types/graphql": "^14.2.2", 31 | "@types/jest": "^24.0.13", 32 | "@types/listr": "^0.14.2", 33 | "@types/node": "^12.0.8", 34 | "@types/nodegit": "^0.24.8", 35 | "@types/ramda": "^0.26.15", 36 | "chai": "^4.2.0", 37 | "commitizen": "^4.0.3", 38 | "cz-conventional-changelog": "3.0.2", 39 | "jest": "^24.8.0", 40 | "prettier": "^1.18.2", 41 | "semantic-release": "^15.13.24", 42 | "ts-jest": "^24.0.2", 43 | "ts-node": "^8", 44 | "tslint": "^5", 45 | "tslint-config-prettier": "^1.18.0" 46 | }, 47 | "engines": { 48 | "node": ">=8.0.0" 49 | }, 50 | "files": [ 51 | "bin", 52 | "lib", 53 | "build" 54 | ], 55 | "homepage": "https://github.com/CDThomas/graphql-usage", 56 | "keywords": [ 57 | "oclif" 58 | ], 59 | "license": "MIT", 60 | "main": "lib/index.js", 61 | "oclif": { 62 | "bin": "graphql-usage" 63 | }, 64 | "repository": "CDThomas/graphql-usage", 65 | "scripts": { 66 | "build": "rm -rf lib && tsc -b && yarn ui:build && yarn ui:copy", 67 | "ui:build": "cd ./graphql-usage-ui && rm -rf build && yarn build", 68 | "ui:copy": "rm -rf ./build && cp -R ./graphql-usage-ui/build ./build", 69 | "posttest": "tslint -p . -t stylish", 70 | "test": "jest", 71 | "test:watch": "jest --watch", 72 | "cm": "git-cz" 73 | }, 74 | "types": "lib/index.d.ts", 75 | "config": { 76 | "commitizen": { 77 | "path": "./node_modules/cz-conventional-changelog" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/findJSGraphQLTags.test.ts: -------------------------------------------------------------------------------- 1 | import findJSGraphQLTags from "./findJSGraphQLTags"; 2 | 3 | // TODO: add test for queries/mutations/subscriptions without names 4 | // TODO: add test for subscriptions 5 | 6 | describe("findJSGraphQLTags", () => { 7 | test("returns GraphQL tags given JS source code", () => { 8 | const js = ` 9 | import { graphql } from 'react-relay'; 10 | const query = graphql\`query findJSGraphQLTagsQuery { hero { id } }\` 11 | `; 12 | 13 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 14 | { 15 | sourceLocationOffset: { 16 | column: 29, 17 | line: 3 18 | }, 19 | template: "query findJSGraphQLTagsQuery { hero { id } }", 20 | filePath: "Component.js" 21 | } 22 | ]); 23 | }); 24 | 25 | test("returns GraphQL tags containing fragments", () => { 26 | const js = ` 27 | import {createFragmentContainer, graphql} from 'react-relay'; 28 | import TodoItem from './TodoItem' 29 | 30 | export default createFragmentContainer(TodoItem, { 31 | hero: graphql\`fragment Hero_hero on Hero { id }\`, 32 | }); 33 | `; 34 | 35 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 36 | { 37 | sourceLocationOffset: { 38 | column: 21, 39 | line: 6 40 | }, 41 | template: "fragment Hero_hero on Hero { id }", 42 | filePath: "Component.js" 43 | } 44 | ]); 45 | }); 46 | 47 | test("returns GraphQL tags containing mutations", () => { 48 | const js = ` 49 | import {commitMutation, graphql} from 'react-relay'; 50 | 51 | const mutation = 52 | graphql\`mutation TestMutation($input: ReviewInput!) { createReview(review: $input) { commentary } }\`; 53 | `; 54 | 55 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 56 | { 57 | sourceLocationOffset: { 58 | column: 15, 59 | line: 5 60 | }, 61 | template: 62 | "mutation TestMutation($input: ReviewInput!) { createReview(review: $input) { commentary } }", 63 | filePath: "Component.js" 64 | } 65 | ]); 66 | }); 67 | 68 | test("returns GraphQL tags given source code with multiple tags", () => { 69 | const js = ` 70 | import { graphql } from 'react-relay'; 71 | const queryOne = graphql\`query firstQuery { hero { id } }\` 72 | const queryTwo = graphql\`query secondQuery { hero { name } }\` 73 | `; 74 | 75 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 76 | { 77 | sourceLocationOffset: { 78 | column: 32, 79 | line: 3 80 | }, 81 | template: "query firstQuery { hero { id } }", 82 | filePath: "Component.js" 83 | }, 84 | { 85 | sourceLocationOffset: { 86 | column: 32, 87 | line: 4 88 | }, 89 | template: "query secondQuery { hero { name } }", 90 | filePath: "Component.js" 91 | } 92 | ]); 93 | }); 94 | 95 | test("returns GraphQL tags for graphql-tag tags", () => { 96 | const js = ` 97 | import gql from "graphql-tag"; 98 | const query = gql\`query findJSGraphQLTagsQuery { hero { id } }\` 99 | `; 100 | 101 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 102 | { 103 | sourceLocationOffset: { 104 | column: 25, 105 | line: 3 106 | }, 107 | template: "query findJSGraphQLTagsQuery { hero { id } }", 108 | filePath: "Component.js" 109 | } 110 | ]); 111 | }); 112 | 113 | test("returns GraphQL tags for Apollo-style fragments", () => { 114 | const js = ` 115 | export const COMMENT_QUERY = gql\` 116 | query Comment($repoName: String!) { 117 | entry(repoFullName: $repoName) { 118 | comments { 119 | ...CommentsPageComment 120 | } 121 | } 122 | } 123 | \${CommentsPage.fragments.comment} 124 | \`; 125 | `; 126 | 127 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 128 | { 129 | sourceLocationOffset: { 130 | column: 40, 131 | line: 2 132 | }, 133 | template: ` 134 | query Comment($repoName: String!) { 135 | entry(repoFullName: $repoName) { 136 | comments { 137 | ...CommentsPageComment 138 | } 139 | } 140 | } 141 | `, 142 | filePath: "Component.js" 143 | } 144 | ]); 145 | }); 146 | 147 | test("returns GraphQL tags for nested Apollo-style fragments", () => { 148 | const js = ` 149 | FeedEntry.fragments = { 150 | entry: gql\` 151 | fragment FeedEntry on Entry { 152 | commentCount 153 | repository { 154 | full_name 155 | html_url 156 | owner { 157 | avatar_url 158 | } 159 | } 160 | ...VoteButtons 161 | ...RepoInfo 162 | } 163 | \${VoteButtons.fragments.entry} 164 | \${RepoInfo.fragments.entry} 165 | \`, 166 | }; 167 | `; 168 | 169 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 170 | { 171 | sourceLocationOffset: { 172 | column: 20, 173 | line: 3 174 | }, 175 | template: ` 176 | fragment FeedEntry on Entry { 177 | commentCount 178 | repository { 179 | full_name 180 | html_url 181 | owner { 182 | avatar_url 183 | } 184 | } 185 | ...VoteButtons 186 | ...RepoInfo 187 | } 188 | `, 189 | filePath: "Component.js" 190 | } 191 | ]); 192 | }); 193 | 194 | test("returns GraphQL tags for tags containing both a query and fragment", () => { 195 | const js = ` 196 | export const COMMENT_QUERY = gql\` 197 | query Comment($repoName: String!) { 198 | entry(repoFullName: $repoName) { 199 | comments { 200 | ...CommentsPageComment 201 | } 202 | } 203 | } 204 | 205 | fragment CommentsPageComment on Comment { 206 | id 207 | postedBy { 208 | login 209 | html_url 210 | } 211 | createdAt 212 | content 213 | } 214 | \`; 215 | `; 216 | 217 | expect(findJSGraphQLTags(js, "Component.js")).toEqual([ 218 | { 219 | sourceLocationOffset: { 220 | column: 40, 221 | line: 2 222 | }, 223 | template: ` 224 | query Comment($repoName: String!) { 225 | entry(repoFullName: $repoName) { 226 | comments { 227 | ...CommentsPageComment 228 | } 229 | } 230 | } 231 | 232 | fragment CommentsPageComment on Comment { 233 | id 234 | postedBy { 235 | login 236 | html_url 237 | } 238 | createdAt 239 | content 240 | } 241 | `, 242 | filePath: "Component.js" 243 | } 244 | ]); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /src/findJSGraphQLTags.ts: -------------------------------------------------------------------------------- 1 | import { parse, ParserOptions, ParserPlugin } from "@babel/parser"; 2 | import traverse, { NodePath } from "@babel/traverse"; 3 | import { 4 | Expression, 5 | TaggedTemplateExpression, 6 | TemplateLiteral 7 | } from "@babel/types"; 8 | 9 | import { GraphQLTag } from "./types"; 10 | 11 | // https://github.com/facebook/relay/blob/master/packages/relay-compiler/language/javascript/FindGraphQLTags.js 12 | 13 | const plugins: ParserPlugin[] = [ 14 | "asyncGenerators", 15 | "classProperties", 16 | ["decorators", { decoratorsBeforeExport: true }], 17 | "doExpressions", 18 | "dynamicImport", 19 | "flow", 20 | "functionBind", 21 | "functionSent", 22 | "jsx", 23 | "nullishCoalescingOperator", 24 | "objectRestSpread", 25 | "optionalChaining", 26 | "optionalCatchBinding" 27 | ]; 28 | 29 | const PARSER_OPTIONS: ParserOptions = { 30 | allowImportExportEverywhere: true, 31 | allowReturnOutsideFunction: true, 32 | allowSuperOutsideMethod: true, 33 | sourceType: "module", 34 | plugins, 35 | strictMode: false 36 | }; 37 | 38 | function findGraphQLTags(code: string, filePath: string): GraphQLTag[] { 39 | const results: GraphQLTag[] = []; 40 | const ast = parse(code, PARSER_OPTIONS); 41 | 42 | const visitors = { 43 | TaggedTemplateExpression: ({ 44 | node 45 | }: NodePath) => { 46 | if (isGraphQLTag(node.tag)) { 47 | results.push({ 48 | template: node.quasi.quasis[0].value.raw, 49 | sourceLocationOffset: getSourceLocationOffset(node.quasi), 50 | filePath 51 | }); 52 | } 53 | } 54 | }; 55 | 56 | traverse(ast, visitors); 57 | return results; 58 | } 59 | 60 | function isGraphQLTag(tag: Expression): boolean { 61 | return ( 62 | tag.type === "Identifier" && (tag.name === "graphql" || tag.name === "gql") 63 | ); 64 | } 65 | 66 | function getSourceLocationOffset( 67 | quasi: TemplateLiteral 68 | ): { line: number; column: number } { 69 | const loc = quasi.quasis[0].loc; 70 | 71 | if (!loc) { 72 | throw new Error( 73 | "findGraphQLTags: Expects template element to have a location" 74 | ); 75 | } 76 | 77 | const start = loc.start; 78 | return { 79 | line: start.line, 80 | column: start.column + 1 // babylon is 0-indexed, graphql expects 1-indexed 81 | }; 82 | } 83 | 84 | export default findGraphQLTags; 85 | -------------------------------------------------------------------------------- /src/findTSGraphQLTags.test.ts: -------------------------------------------------------------------------------- 1 | import findTSGraphQLTags from "./findTSGraphQLTags"; 2 | 3 | describe("findTSGraphQLTags", () => { 4 | test("returns GraphQL tags given TS source code", () => { 5 | const ts = ` 6 | import { graphql } from 'react-relay'; 7 | const query = graphql\`query findGraphQLTagsQuery { hero { id } }\` 8 | `; 9 | 10 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 11 | { 12 | sourceLocationOffset: { 13 | column: 29, 14 | line: 3 15 | }, 16 | template: "query findGraphQLTagsQuery { hero { id } }", 17 | filePath: "Component.tsx" 18 | } 19 | ]); 20 | }); 21 | 22 | test("returns GraphQL tags containing fragments", () => { 23 | const ts = ` 24 | import {createFragmentContainer, graphql} from 'react-relay'; 25 | import TodoItem from './TodoItem' 26 | 27 | export default createFragmentContainer(TodoItem, { 28 | hero: graphql\`fragment Hero_hero on Hero { id }\`, 29 | }); 30 | `; 31 | 32 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 33 | { 34 | sourceLocationOffset: { 35 | column: 21, 36 | line: 6 37 | }, 38 | template: "fragment Hero_hero on Hero { id }", 39 | filePath: "Component.tsx" 40 | } 41 | ]); 42 | }); 43 | 44 | test("returns GraphQL tags containing mutations", () => { 45 | const ts = ` 46 | import {commitMutation, graphql} from 'react-relay'; 47 | 48 | const mutation = 49 | graphql\`mutation TestMutation($input: ReviewInput!) { createReview(review: $input) { commentary } }\`; 50 | `; 51 | 52 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 53 | { 54 | sourceLocationOffset: { 55 | column: 15, 56 | line: 5 57 | }, 58 | template: 59 | "mutation TestMutation($input: ReviewInput!) { createReview(review: $input) { commentary } }", 60 | filePath: "Component.tsx" 61 | } 62 | ]); 63 | }); 64 | 65 | test("returns GraphQL tags given source code with multiple tags", () => { 66 | const ts = ` 67 | import { graphql } from 'react-relay'; 68 | const queryOne = graphql\`query firstQuery { hero { id } }\` 69 | const queryTwo = graphql\`query secondQuery { hero { name } }\` 70 | `; 71 | 72 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 73 | { 74 | sourceLocationOffset: { 75 | column: 32, 76 | line: 3 77 | }, 78 | template: "query firstQuery { hero { id } }", 79 | filePath: "Component.tsx" 80 | }, 81 | { 82 | sourceLocationOffset: { 83 | column: 32, 84 | line: 4 85 | }, 86 | template: "query secondQuery { hero { name } }", 87 | filePath: "Component.tsx" 88 | } 89 | ]); 90 | }); 91 | 92 | test("returns GraphQL tags for graphql-tag tags", () => { 93 | const ts = ` 94 | import gql from "graphql-tag"; 95 | const query = gql\`query findGraphQLTagsQuery { hero { id } }\` 96 | `; 97 | 98 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 99 | { 100 | sourceLocationOffset: { 101 | column: 25, 102 | line: 3 103 | }, 104 | template: "query findGraphQLTagsQuery { hero { id } }", 105 | filePath: "Component.tsx" 106 | } 107 | ]); 108 | }); 109 | 110 | test("returns GraphQL tags for Apollo-style fragments", () => { 111 | const ts = ` 112 | export const COMMENT_QUERY = gql\` 113 | query Comment($repoName: String!) { 114 | entry(repoFullName: $repoName) { 115 | comments { 116 | ...CommentsPageComment 117 | } 118 | } 119 | } 120 | \${CommentsPage.fragments.comment} 121 | \`; 122 | `; 123 | 124 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 125 | { 126 | sourceLocationOffset: { 127 | column: 40, 128 | line: 2 129 | }, 130 | template: ` 131 | query Comment($repoName: String!) { 132 | entry(repoFullName: $repoName) { 133 | comments { 134 | ...CommentsPageComment 135 | } 136 | } 137 | } 138 | `, 139 | filePath: "Component.tsx" 140 | } 141 | ]); 142 | }); 143 | 144 | test("returns GraphQL tags for nested Apollo-style fragments", () => { 145 | const ts = ` 146 | FeedEntry.fragments = { 147 | entry: gql\` 148 | fragment FeedEntry on Entry { 149 | commentCount 150 | repository { 151 | full_name 152 | html_url 153 | owner { 154 | avatar_url 155 | } 156 | } 157 | ...VoteButtons 158 | ...RepoInfo 159 | } 160 | \${VoteButtons.fragments.entry} 161 | \${RepoInfo.fragments.entry} 162 | \`, 163 | }; 164 | `; 165 | 166 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 167 | { 168 | sourceLocationOffset: { 169 | column: 20, 170 | line: 3 171 | }, 172 | template: ` 173 | fragment FeedEntry on Entry { 174 | commentCount 175 | repository { 176 | full_name 177 | html_url 178 | owner { 179 | avatar_url 180 | } 181 | } 182 | ...VoteButtons 183 | ...RepoInfo 184 | } 185 | `, 186 | filePath: "Component.tsx" 187 | } 188 | ]); 189 | }); 190 | 191 | test("returns GraphQL tags for tags containing both a query and fragment", () => { 192 | const ts = ` 193 | export const COMMENT_QUERY = gql\` 194 | query Comment($repoName: String!) { 195 | entry(repoFullName: $repoName) { 196 | comments { 197 | ...CommentsPageComment 198 | } 199 | } 200 | } 201 | 202 | fragment CommentsPageComment on Comment { 203 | id 204 | postedBy { 205 | login 206 | html_url 207 | } 208 | createdAt 209 | content 210 | } 211 | \`; 212 | `; 213 | 214 | expect(findTSGraphQLTags(ts, "Component.tsx")).toEqual([ 215 | { 216 | sourceLocationOffset: { 217 | column: 40, 218 | line: 2 219 | }, 220 | template: ` 221 | query Comment($repoName: String!) { 222 | entry(repoFullName: $repoName) { 223 | comments { 224 | ...CommentsPageComment 225 | } 226 | } 227 | } 228 | 229 | fragment CommentsPageComment on Comment { 230 | id 231 | postedBy { 232 | login 233 | html_url 234 | } 235 | createdAt 236 | content 237 | } 238 | `, 239 | filePath: "Component.tsx" 240 | } 241 | ]); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /src/findTSGraphQLTags.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createSourceFile, 3 | forEachChild, 4 | Identifier, 5 | isNoSubstitutionTemplateLiteral, 6 | isTemplateExpression, 7 | Node, 8 | NoSubstitutionTemplateLiteral, 9 | ScriptTarget, 10 | SyntaxKind, 11 | TaggedTemplateExpression, 12 | TemplateHead 13 | } from "typescript"; 14 | 15 | import { GraphQLTag } from "./types"; 16 | 17 | // https://github.com/relay-tools/relay-compiler-language-typescript/blob/d3c7af5d5558569def7c46d485b7fc64c6a94ccb/src/FindGraphQLTags.ts 18 | 19 | function visit( 20 | node: Node, 21 | filePath: string, 22 | addGraphQLTag: (tag: GraphQLTag) => void 23 | ): void { 24 | function visitNode(node: Node) { 25 | switch (node.kind) { 26 | case SyntaxKind.TaggedTemplateExpression: { 27 | const taggedTemplate = node as TaggedTemplateExpression; 28 | if (isGraphQLTag(taggedTemplate.tag)) { 29 | addGraphQLTag({ 30 | template: getGraphQLText(taggedTemplate), 31 | sourceLocationOffset: getSourceLocationOffset(taggedTemplate), 32 | filePath 33 | }); 34 | } 35 | } 36 | } 37 | forEachChild(node, visitNode); 38 | } 39 | 40 | visitNode(node); 41 | } 42 | 43 | function isGraphQLTag(tag: Node): boolean { 44 | return ( 45 | tag.kind === SyntaxKind.Identifier && 46 | ((tag as Identifier).text === "graphql" || 47 | (tag as Identifier).text === "gql") 48 | ); 49 | } 50 | 51 | function getTemplateNode( 52 | quasi: TaggedTemplateExpression 53 | ): NoSubstitutionTemplateLiteral | TemplateHead { 54 | if (isNoSubstitutionTemplateLiteral(quasi.template)) { 55 | return quasi.template as NoSubstitutionTemplateLiteral; 56 | } 57 | 58 | if (isTemplateExpression(quasi.template)) { 59 | return quasi.template.head; 60 | } 61 | 62 | throw new Error( 63 | "findTSGraphQLTags: getTemplateNode expects a TaggedTemplateExpression" 64 | ); 65 | } 66 | 67 | function getGraphQLText(quasi: TaggedTemplateExpression) { 68 | return getTemplateNode(quasi).text; 69 | } 70 | 71 | function getSourceLocationOffset(quasi: TaggedTemplateExpression) { 72 | const pos = quasi.template.pos; 73 | const loc = quasi.getSourceFile().getLineAndCharacterOfPosition(pos); 74 | 75 | return { 76 | line: loc.line + 1, 77 | column: loc.character + 2 78 | }; 79 | } 80 | 81 | function find(text: string, filePath: string) { 82 | const result: GraphQLTag[] = []; 83 | const ast = createSourceFile(filePath, text, ScriptTarget.Latest, true); 84 | visit(ast, filePath, tag => result.push(tag)); 85 | return result; 86 | } 87 | 88 | export default find; 89 | -------------------------------------------------------------------------------- /src/flatten.ts: -------------------------------------------------------------------------------- 1 | function flatten(array: Array>): Array { 2 | return array.reduce( 3 | (accumulator: Array, current: Array) => accumulator.concat(current), 4 | [] 5 | ); 6 | } 7 | 8 | export default flatten; 9 | -------------------------------------------------------------------------------- /src/getFieldInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldNode, 3 | FragmentDefinitionNode, 4 | getLocation, 5 | OperationDefinitionNode, 6 | parse, 7 | Source, 8 | TypeInfo, 9 | visit, 10 | visitWithTypeInfo 11 | } from "graphql"; 12 | 13 | import { GraphQLTag } from "./types"; 14 | 15 | export interface FieldInfo { 16 | name: string; 17 | line: number; 18 | parentType: string; 19 | type: string; 20 | rootNodeName: string; 21 | filePath: string; 22 | } 23 | 24 | function findFields( 25 | graphQLTag: GraphQLTag, 26 | typeInfo: TypeInfo, 27 | cb: (fieldInfo: FieldInfo) => void 28 | ) { 29 | const ast = parse(graphQLTag.template); 30 | 31 | visit( 32 | ast, 33 | visitWithTypeInfo(typeInfo, { 34 | OperationDefinition(node) { 35 | visitFields(node, graphQLTag, typeInfo, cb); 36 | }, 37 | FragmentDefinition(node) { 38 | visitFields(node, graphQLTag, typeInfo, cb); 39 | } 40 | }) 41 | ); 42 | } 43 | 44 | function visitFields( 45 | node: OperationDefinitionNode | FragmentDefinitionNode, 46 | graphQLTag: GraphQLTag, 47 | typeInfo: TypeInfo, 48 | cb: (fieldInfo: FieldInfo) => void 49 | ) { 50 | if (!node.name) { 51 | throw new Error( 52 | "visitFields expects OperationDefinitions and FragmentDefinitions to be named" 53 | ); 54 | } 55 | 56 | const { filePath, sourceLocationOffset, template } = graphQLTag; 57 | const operationOrFragmentName = node.name.value; 58 | 59 | visit( 60 | node, 61 | visitWithTypeInfo(typeInfo, { 62 | Field(graphqlNode) { 63 | // Discard client only fields, but don't throw an error 64 | if (isClientOnlyField(graphqlNode)) return; 65 | 66 | const parentType = typeInfo.getParentType(); 67 | const nodeType = typeInfo.getType(); 68 | const nodeName = graphqlNode.name.value; 69 | 70 | if (!parentType) { 71 | throw new Error( 72 | `visitFields expects fields to have a parent type. No parent type for ${nodeName}` 73 | ); 74 | } 75 | 76 | if (!nodeType) { 77 | throw new Error( 78 | `visitFields expects fields to have a type. No type for ${nodeName}` 79 | ); 80 | } 81 | 82 | if (!graphqlNode.loc) { 83 | throw new Error( 84 | `visitFields expects fields to have a location. No location for ${nodeName}` 85 | ); 86 | } 87 | 88 | const loc = graphqlNode.loc; 89 | const source = new Source(template); 90 | const templateStart = getLocation(source, loc.start); 91 | const line = sourceLocationOffset.line + templateStart.line - 1; 92 | 93 | cb({ 94 | name: nodeName, 95 | type: nodeType.toString(), 96 | parentType: parentType.toString(), 97 | rootNodeName: operationOrFragmentName, 98 | filePath, 99 | line 100 | }); 101 | } 102 | }) 103 | ); 104 | } 105 | 106 | function isClientOnlyField(field: FieldNode): boolean { 107 | if (!field.directives) return false; 108 | 109 | const clientOnlyDirective = field.directives.find(directive => { 110 | return directive.name.value === "client"; 111 | }); 112 | 113 | return !!clientOnlyDirective; 114 | } 115 | 116 | export default findFields; 117 | -------------------------------------------------------------------------------- /src/gitUtils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import path from "path"; 3 | import { promisify } from "util"; 4 | 5 | async function getGitHubBaseURL(sourceDir: string): Promise { 6 | const branchName = await execCommand( 7 | "git rev-parse --abbrev-ref HEAD", 8 | sourceDir 9 | ); 10 | 11 | if (!branchName) { 12 | throw new Error("Error getting current Git branch name"); 13 | } 14 | 15 | const remoteURL = await execCommand( 16 | "git config --get remote.origin.url", 17 | sourceDir 18 | ); 19 | const repoBasePathRegEx = /^git@github\.com:(.*)\.git$/; 20 | const matches = remoteURL.match(repoBasePathRegEx); 21 | 22 | if (!matches || !matches[1]) { 23 | throw new Error( 24 | "Error getting remote URL. GraphQL Usage requires SOURCE_DIR to be in a Git repository " + 25 | "that has been pushed to GitHub." 26 | ); 27 | } 28 | 29 | const repoBasePath = matches[1]; 30 | 31 | return `https://github.com/${repoBasePath}/tree/${branchName}`; 32 | } 33 | 34 | async function getGitProjectRoot(sourceDir: string): Promise { 35 | const projectRoot = execCommand("git rev-parse --show-toplevel", sourceDir); 36 | 37 | if (!projectRoot) { 38 | throw new Error("Error getting Git project root"); 39 | } 40 | 41 | return projectRoot; 42 | } 43 | 44 | function execCommand(command: string, sourceDir: string): Promise { 45 | const message = 46 | "GraphQL Usage requires git to be available in your PATH " + 47 | "and for SOURCE_DIR to be in a Git repository that has been pushed to GitHub."; 48 | 49 | return promisify(exec)(command, { cwd: path.resolve(sourceDir) }) 50 | .then(({ stdout }) => { 51 | return stdout.trim(); 52 | }) 53 | .catch(error => { 54 | if (error.code === 127) { 55 | throw new Error(`Command not found: git. ${message}`); 56 | } 57 | 58 | if (error.code === 128) { 59 | throw new Error( 60 | `SOURCE_DIRECTORY is not in a Git repository or Git has encountered a fatal error. ${message}` 61 | ); 62 | } 63 | 64 | throw error; 65 | }); 66 | } 67 | 68 | export { getGitHubBaseURL, getGitProjectRoot }; 69 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@oclif/test"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { omit } from "ramda"; 5 | 6 | import cmd = require("../src"); 7 | 8 | // https://github.com/apollographql/apollo-tooling/blob/e8d432654ea840b9d59bf1f22a9bc37cf50cf800/packages/apollo/src/commands/client/__tests__/generate.test.ts 9 | 10 | const deleteFolderRecursive = (path: string) => { 11 | if (fs.existsSync(path)) { 12 | fs.readdirSync(path).forEach(function(file) { 13 | const curPath = path + "/" + file; 14 | if (fs.lstatSync(curPath).isDirectory()) { 15 | // recurse 16 | deleteFolderRecursive(curPath); 17 | } else { 18 | // delete file 19 | fs.unlinkSync(curPath); 20 | } 21 | }); 22 | 23 | fs.rmdirSync(path); 24 | } 25 | }; 26 | 27 | const makeNestedDir = (dir: string) => { 28 | if (fs.existsSync(dir)) return; 29 | 30 | try { 31 | fs.mkdirSync(dir); 32 | } catch (err) { 33 | if (err.code === "ENOENT") { 34 | makeNestedDir(path.dirname(dir)); //create parent dir 35 | makeNestedDir(dir); //create dir 36 | } 37 | } 38 | }; 39 | 40 | const setupFS = (files: Record) => { 41 | let dir: string | undefined; 42 | return { 43 | async run() { 44 | // make a random temp dir & chdir into it 45 | dir = fs.mkdtempSync("__tmp__"); 46 | process.chdir(dir); 47 | // fill the dir with `files` 48 | Object.keys(files).forEach(key => { 49 | if (key.includes("/")) makeNestedDir(path.dirname(key)); 50 | fs.writeFileSync(key, files[key]); 51 | }); 52 | }, 53 | finally() { 54 | process.chdir("../"); 55 | deleteFolderRecursive(dir as string); 56 | } 57 | }; 58 | }; 59 | 60 | // helper function to resolve files from the actual filesystem 61 | const resolveFiles = (opts: { [testPath: string]: string }) => { 62 | let files: { [testPath: string]: string } = {}; 63 | Object.keys(opts).map(key => { 64 | files[key] = fs.readFileSync(path.resolve(__dirname, opts[key]), { 65 | encoding: "utf-8" 66 | }); 67 | }); 68 | 69 | return files; 70 | }; 71 | 72 | describe("graphql-usage", () => { 73 | test 74 | .register("fs", setupFS) 75 | .fs( 76 | resolveFiles({ 77 | "./schema.json": "../__fixtures__/schema.json", 78 | "./testSrc/foo.js": "../__fixtures__/testSrc/foo.js" 79 | }) 80 | ) 81 | .do(() => cmd.run(["./schema.json", "./testSrc", "--json", "--quiet"])) 82 | .it("writes a file given a .json GraphQL schema", () => { 83 | const output = JSON.parse(fs.readFileSync("./report.json", "utf-8")); 84 | 85 | assertOutputMatchesSnapshot(output); 86 | }); 87 | 88 | test 89 | .register("fs", setupFS) 90 | .fs( 91 | resolveFiles({ 92 | "./schema.graphql": "../__fixtures__/schema.graphql", 93 | "./testSrc/foo.js": "../__fixtures__/testSrc/foo.js" 94 | }) 95 | ) 96 | .do(() => cmd.run(["./schema.graphql", "./testSrc", "--json", "--quiet"])) 97 | .it("writes a file given a .graphql GraphQL schema", () => { 98 | const output = JSON.parse(fs.readFileSync("./report.json", "utf-8")); 99 | 100 | assertOutputMatchesSnapshot(output); 101 | }); 102 | 103 | test 104 | .register("fs", setupFS) 105 | .fs( 106 | resolveFiles({ 107 | "./schema.graphql": "../__fixtures__/schema.graphql", 108 | "./testSrc/foo.js": "../__fixtures__/testSrc/foo.js", 109 | "./testSrc/nestedDir/bar.js": "../__fixtures__/testSrc/foo.js" 110 | }) 111 | ) 112 | .do(() => cmd.run(["./schema.graphql", "./testSrc", "--json", "--quiet"])) 113 | .it("reports on files in nested directories", () => { 114 | const output = JSON.parse(fs.readFileSync("./report.json", "utf-8")); 115 | 116 | assertOutputMatchesSnapshot(output); 117 | }); 118 | 119 | test 120 | .register("fs", setupFS) 121 | .fs( 122 | resolveFiles({ 123 | "./schema.graphql": "../__fixtures__/schema.graphql", 124 | "./testSrc/foo.jsx": "../__fixtures__/testSrc/foo.js" 125 | }) 126 | ) 127 | .do(() => cmd.run(["./schema.graphql", "./testSrc", "--json", "--quiet"])) 128 | .it("reports on jsx files", () => { 129 | const output = JSON.parse(fs.readFileSync("./report.json", "utf-8")); 130 | 131 | assertOutputMatchesSnapshot(output); 132 | }); 133 | 134 | test 135 | .register("fs", setupFS) 136 | .fs( 137 | resolveFiles({ 138 | "./schema.graphql": "../__fixtures__/schema.graphql", 139 | "./testSrc/foo.ts": "../__fixtures__/testSrc/foo.js" 140 | }) 141 | ) 142 | .do(() => cmd.run(["./schema.graphql", "./testSrc", "--json", "--quiet"])) 143 | .it("reports on ts files", () => { 144 | const output = JSON.parse(fs.readFileSync("./report.json", "utf-8")); 145 | 146 | assertOutputMatchesSnapshot(output); 147 | }); 148 | 149 | test 150 | .register("fs", setupFS) 151 | .fs( 152 | resolveFiles({ 153 | "./schema.graphql": "../__fixtures__/schema.graphql", 154 | "./testSrc/foo.tsx": "../__fixtures__/testSrc/foo.js" 155 | }) 156 | ) 157 | .do(() => cmd.run(["./schema.graphql", "./testSrc", "--json", "--quiet"])) 158 | .it("reports on tsx files", () => { 159 | const output = JSON.parse(fs.readFileSync("./report.json", "utf-8")); 160 | 161 | assertOutputMatchesSnapshot(output); 162 | }); 163 | 164 | test 165 | .register("fs", setupFS) 166 | .fs( 167 | resolveFiles({ 168 | "./schema.graphql": "../__fixtures__/schema.graphql", 169 | "./testSrc/node_modules/foo.js": "../__fixtures__/testSrc/foo.js", 170 | "./testSrc/__mocks__/foo.js": "../__fixtures__/testSrc/foo.js", 171 | "./testSrc/__generated__/foo.js": "../__fixtures__/testSrc/foo.js", 172 | "./testSrc/__tests__/foo.js": "../__fixtures__/testSrc/foo.js", 173 | "./testSrc/foo.test.js": "../__fixtures__/testSrc/foo.js", 174 | "./testSrc/foo.test.jsx": "../__fixtures__/testSrc/foo.js", 175 | "./testSrc/foo.test.ts": "../__fixtures__/testSrc/foo.js", 176 | "./testSrc/foo.test.tsx": "../__fixtures__/testSrc/foo.js" 177 | }) 178 | ) 179 | .do(() => cmd.run(["./schema.graphql", "./testSrc", "--json", "--quiet"])) 180 | .it("provides default exclude", () => { 181 | const output = fs.readFileSync("./report.json", "utf-8"); 182 | 183 | [ 184 | "node_modules", 185 | "__mocks__", 186 | "__generated__", 187 | "__tests__", 188 | ".test.js", 189 | ".test.jsx", 190 | ".test.ts", 191 | ".test.tsx" 192 | ].forEach(exclude => { 193 | expect(output).not.toContain(exclude); 194 | }); 195 | }); 196 | 197 | test 198 | .register("fs", setupFS) 199 | .fs( 200 | resolveFiles({ 201 | "./schema.graphql": "../__fixtures__/schema.graphql", 202 | "./testSrc/node_modules/foo.js": "../__fixtures__/testSrc/foo.js", 203 | "./testSrc/__mocks__/foo.js": "../__fixtures__/testSrc/foo.js", 204 | "./testSrc/__generated__/foo.js": "../__fixtures__/testSrc/foo.js", 205 | "./testSrc/__tests__/foo.js": "../__fixtures__/testSrc/foo.js", 206 | "./testSrc/foo.test.js": "../__fixtures__/testSrc/foo.js", 207 | "./testSrc/other_dir/foo.js": "../__fixtures__/testSrc/foo.js" 208 | }) 209 | ) 210 | .do(() => 211 | cmd.run([ 212 | "./schema.graphql", 213 | "./testSrc", 214 | "--json", 215 | "--quiet", 216 | "--exclude", 217 | "**/other_dir/**" 218 | ]) 219 | ) 220 | .it("uses provided exclude over defaults", () => { 221 | const output = fs.readFileSync("./report.json", "utf-8"); 222 | 223 | [ 224 | "node_modules", 225 | "__mocks__", 226 | "__generated__", 227 | "__tests__", 228 | ".test.js" 229 | ].forEach(exclude => { 230 | expect(output).toContain(exclude); 231 | }); 232 | 233 | expect(output).not.toContain("other_dir"); 234 | }); 235 | }); 236 | 237 | function assertOutputMatchesSnapshot(output: { 238 | data: { types: Array }; 239 | }) { 240 | output.data.types.map((type: any) => { 241 | expect(omit(["fields"], type)).toMatchSnapshot(); 242 | 243 | type.fields.map((field: any) => { 244 | expect(omit(["occurrences"], field)).toMatchSnapshot(); 245 | 246 | field.occurrences.map((occurrence: any) => { 247 | expect(occurrence).toMatchSnapshot({ 248 | filename: expect.stringMatching( 249 | /^https:\/\/github.com\/CDThomas\/graphql-usage\/tree\/.*\.(js|jsx|ts|tsx)#L\d$/ 250 | ) 251 | }); 252 | }); 253 | }); 254 | }); 255 | } 256 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from "@oclif/command"; 2 | import glob from "fast-glob"; 3 | import fs from "fs"; 4 | import { 5 | buildClientSchema, 6 | buildSchema, 7 | GraphQLSchema, 8 | TypeInfo 9 | } from "graphql"; 10 | import Listr from "listr"; 11 | import open from "open"; 12 | import path from "path"; 13 | import { partialRight, unary } from "ramda"; 14 | import { promisify } from "util"; 15 | 16 | import findJSGraphQLTags from "./findJSGraphQLTags"; 17 | import findTSGraphQLTags from "./findTSGraphQLTags"; 18 | import flatten from "./flatten"; 19 | import getFeildInfo, { FieldInfo } from "./getFieldInfo"; 20 | import { getGitHubBaseURL, getGitProjectRoot } from "./gitUtils"; 21 | import { addOccurrence, buildInitialState, format, Report } from "./report"; 22 | import createServer from "./server"; 23 | import { GraphQLTag } from "./types"; 24 | 25 | class GraphqlStats extends Command { 26 | static description = 27 | "Analyzes JS source files and generates a report on GraphQL field usage."; 28 | 29 | static examples = ["$ graphql-usage ./schema.json ./src/"]; 30 | 31 | static flags = { 32 | exclude: flags.string({ 33 | // Default provided in `run` method 34 | description: "Directories to ignore under src", 35 | multiple: true 36 | }), 37 | port: flags.integer({ 38 | description: "Port to run the report server on", 39 | default: 3001 40 | }), 41 | quiet: flags.boolean({ 42 | description: "No output to stdout", 43 | default: false 44 | }), 45 | 46 | // Meta flags 47 | help: flags.help({ char: "h" }), 48 | version: flags.version({ char: "v" }), 49 | 50 | // Hidden flags 51 | json: flags.boolean({ 52 | description: "Output report as JSON rather than starting the app", 53 | hidden: true 54 | }) 55 | }; 56 | 57 | static args = [ 58 | { name: "schema", required: true }, 59 | { name: "sourceDir", required: true } 60 | ]; 61 | 62 | async run() { 63 | const { args, flags } = this.parse(GraphqlStats); 64 | const { schema, sourceDir } = args; 65 | const { json, exclude, port, quiet } = flags; 66 | const renderer = quiet ? "silent" : "default"; 67 | 68 | const analyzeFilesTask = { 69 | title: "Analyzing source files ", 70 | task: async (ctx: { report: Report | undefined }) => { 71 | ctx.report = await analyzeFiles(schema, sourceDir, exclude); 72 | } 73 | }; 74 | 75 | const jsonTasks = new Listr( 76 | [ 77 | analyzeFilesTask, 78 | { 79 | title: "Writing JSON", 80 | task: async ({ report }: { report: Report }) => { 81 | await writeJSON(report); 82 | } 83 | } 84 | ], 85 | { renderer } 86 | ); 87 | 88 | const appTasks = new Listr( 89 | [ 90 | analyzeFilesTask, 91 | { 92 | title: `Starting server at http://localhost:${port}`, 93 | task: ({ report }: { report: Report }) => { 94 | startServer(report, port); 95 | } 96 | } 97 | ], 98 | { renderer } 99 | ); 100 | 101 | await (json ? jsonTasks : appTasks).run(); 102 | } 103 | } 104 | 105 | async function analyzeFiles( 106 | schemaFile: string, 107 | sourceDir: string, 108 | exclude?: string[] 109 | ): Promise { 110 | const schema = await readSchema(schemaFile); 111 | 112 | const gitHubBaseURL = await getGitHubBaseURL(sourceDir); 113 | const gitDir = await getGitProjectRoot(sourceDir); 114 | 115 | const extensions = ["js", "jsx", "ts", "tsx"]; 116 | const defaultExclude = [ 117 | // Node modules 118 | "**/node_modules/**", 119 | // Relay compiler artifacts 120 | "**/__generated__/**", 121 | // Test files 122 | "**/__mocks__/**", 123 | "**/__tests__/**", 124 | "**/*.test.(js|jsx|ts|tsx)" 125 | ]; 126 | const files = await glob(`**/*.+(${extensions.join("|")})`, { 127 | cwd: sourceDir, 128 | ignore: exclude || defaultExclude 129 | }); 130 | 131 | const data = await Promise.all( 132 | files.map(unary(partialRight(readFile, [sourceDir]))) 133 | ); 134 | let tags = flatten(data.map(findGraphQLTags)); 135 | 136 | const state = buildInitialState(schema); 137 | findFields( 138 | schema, 139 | tags, 140 | ({ parentType, name, filePath, line, rootNodeName }: FieldInfo) => { 141 | const gitHubFileURL = filePath.replace( 142 | path.resolve(gitDir), 143 | gitHubBaseURL 144 | ); 145 | const link = `${gitHubFileURL}#L${line}`; 146 | 147 | addOccurrence(state, parentType, name, { 148 | filename: link, 149 | rootNodeName 150 | }); 151 | } 152 | ); 153 | 154 | return format(state); 155 | } 156 | 157 | async function readSchema(schemaFile: string): Promise { 158 | const extension = path.extname(schemaFile); 159 | 160 | const schemaString = await promisify(fs.readFile)(schemaFile, { 161 | encoding: "utf-8" 162 | }); 163 | 164 | if (extension === ".json") { 165 | const schemaJSON = JSON.parse(schemaString); 166 | return buildClientSchema(schemaJSON.data); 167 | } 168 | 169 | if (extension === ".graphql") { 170 | return buildSchema(schemaString); 171 | } 172 | 173 | throw new Error( 174 | "Invalid schema file. Please provide a .json or .graphql GraphQL schema." 175 | ); 176 | } 177 | 178 | function writeJSON(report: Report): Promise { 179 | const OUTPUT_FILE = "./report.json"; 180 | 181 | return promisify(fs.writeFile)( 182 | OUTPUT_FILE, 183 | JSON.stringify(report, null, 2), 184 | "utf-8" 185 | ); 186 | } 187 | 188 | function startServer(report: Report, port: number): void { 189 | createServer(report).listen(port, async () => { 190 | // tslint:disable-next-line:no-http-string 191 | await open(`http://localhost:${port}`); 192 | }); 193 | } 194 | 195 | interface SourceFile { 196 | content: string; 197 | extname: string; 198 | fullPath: string; 199 | } 200 | 201 | async function readFile( 202 | filePath: string, 203 | sourceDir: string 204 | ): Promise { 205 | const fullPath = path.resolve(process.cwd(), sourceDir, filePath); 206 | const extname = path.extname(filePath); 207 | const content = await promisify(fs.readFile)(fullPath, { 208 | encoding: "utf-8" 209 | }); 210 | 211 | return { fullPath, extname, content }; 212 | } 213 | 214 | function findGraphQLTags({ 215 | fullPath, 216 | extname, 217 | content 218 | }: SourceFile): GraphQLTag[] { 219 | let tags: GraphQLTag[] | undefined; 220 | if (extname === ".js" || extname === ".jsx") { 221 | tags = findJSGraphQLTags(content, fullPath); 222 | } 223 | if (extname === ".ts" || extname === ".tsx") { 224 | tags = findTSGraphQLTags(content, fullPath); 225 | } 226 | 227 | if (!tags) { 228 | throw new Error("run: analyzeFiles expects a js, jsx, tx, or tsx file"); 229 | } 230 | 231 | return tags; 232 | } 233 | 234 | function findFields( 235 | schema: GraphQLSchema, 236 | tags: GraphQLTag[], 237 | cb: (data: FieldInfo) => void 238 | ): void { 239 | const typeInfo = new TypeInfo(schema); 240 | 241 | tags.forEach((tag: GraphQLTag) => { 242 | getFeildInfo(tag, typeInfo, cb); 243 | }); 244 | } 245 | 246 | export = GraphqlStats; 247 | -------------------------------------------------------------------------------- /src/report.test.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, isObjectType } from "graphql"; 2 | 3 | import { addOccurrence, buildInitialState, format } from "./report"; 4 | 5 | const testSchema = buildSchema(` 6 | type Query { 7 | books: [Book] 8 | } 9 | 10 | type Book { 11 | title: String! 12 | pageCount: Int 13 | isPublished: Boolean 14 | } 15 | `); 16 | 17 | describe("buildInitialState", () => { 18 | test("includes a property for each named type in the schema", () => { 19 | // TODO: non-object types 20 | const expectedTypes = testSchema.toConfig().types.filter(isObjectType); 21 | 22 | const { types } = buildInitialState(testSchema); 23 | 24 | expectedTypes.forEach(type => { 25 | expect(types).toHaveProperty(type.name); 26 | }); 27 | }); 28 | 29 | test("builds the correct properties for Object types", () => { 30 | const { types } = buildInitialState(testSchema); 31 | 32 | expect(types.Book).toEqual({ 33 | fields: expect.any(Object), 34 | // kind: "Object", 35 | name: "Book" 36 | }); 37 | }); 38 | 39 | test("includes a property for each field of an Object type", () => { 40 | const initialState = buildInitialState(testSchema); 41 | const fields = initialState.types.Book.fields; 42 | 43 | expect(fields).toEqual({ 44 | title: expect.any(Object), 45 | pageCount: expect.any(Object), 46 | isPublished: expect.any(Object) 47 | }); 48 | }); 49 | 50 | test("includes the correct properties for individual object fields", () => { 51 | const initialState = buildInitialState(testSchema); 52 | const field = initialState.types.Book.fields.title; 53 | 54 | expect(field).toEqual({ 55 | name: "title", 56 | occurrences: [], 57 | type: { 58 | kind: "NonNull", 59 | name: null, 60 | ofType: { 61 | kind: "Scalar", 62 | name: "String", 63 | ofType: null 64 | } 65 | } 66 | }); 67 | }); 68 | }); 69 | 70 | describe("addOccurrence", () => { 71 | test("adds an occurrence to the given field", () => { 72 | const state = buildInitialState(testSchema); 73 | 74 | addOccurrence(state, "Book", "title", { 75 | filename: "src/Component.js", 76 | rootNodeName: "ComponentQuery" 77 | }); 78 | 79 | expect(state.types.Book.fields.title.occurrences).toEqual([ 80 | { 81 | filename: "src/Component.js", 82 | rootNodeName: "ComponentQuery" 83 | } 84 | ]); 85 | }); 86 | }); 87 | 88 | describe("format", () => { 89 | test("formats types as a list sorted alphabetically", () => { 90 | const state = buildInitialState(testSchema); 91 | const report = format(state); 92 | 93 | expect(report.data.types.map(({ name }) => name)).toEqual([ 94 | "Book", 95 | "Query", 96 | "__Directive", 97 | "__EnumValue", 98 | "__Field", 99 | "__InputValue", 100 | "__Schema", 101 | "__Type" 102 | ]); 103 | }); 104 | 105 | test("formats fields as a list sorted alphabetically", () => { 106 | const state = buildInitialState(testSchema); 107 | const report = format(state); 108 | 109 | const bookType = report.data.types.find(type => type.name === "Book")!; 110 | expect(bookType.fields.map(({ name }) => name)).toEqual([ 111 | "isPublished", 112 | "pageCount", 113 | "title" 114 | ]); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/report.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLField, 3 | GraphQLNamedType, 4 | GraphQLObjectType, 5 | GraphQLSchema, 6 | GraphQLType, 7 | isEnumType, 8 | isInterfaceType, 9 | isListType, 10 | isNamedType, 11 | isNonNullType, 12 | isObjectType, 13 | isScalarType, 14 | isUnionType, 15 | isWrappingType 16 | } from "graphql"; 17 | import R from "ramda"; 18 | 19 | interface ReportAccumulator { 20 | types: ReportAccumulatorTypeMap; 21 | // directives: ReportAccumulatorDirectiveMap; 22 | } 23 | 24 | interface ReportAccumulatorTypeMap { 25 | [key: string]: ReportAccumulatorType; 26 | } 27 | 28 | const enum TypeKind { 29 | Scalar = "Scalar", 30 | Object = "Object", 31 | Interface = "Interface", 32 | Union = "Union", 33 | Enum = "Enum", 34 | InputObject = "InputObject", 35 | List = "List", 36 | NonNull = "NonNull" 37 | } 38 | 39 | type ReportAccumulatorType = ReportAccumulatorObjectType; 40 | 41 | interface ReportAccumulatorObjectType { 42 | fields: ReportAccumulatorFieldMap; 43 | // kind: TypeKind.Object; 44 | name: string; 45 | } 46 | 47 | interface ReportAccumulatorFieldMap { 48 | [key: string]: ReportAccumulatorField; 49 | } 50 | 51 | interface ReportAccumulatorField { 52 | name: string; 53 | occurrences: ReportOccurrence[]; 54 | type: ReportAccumulatorOfType; 55 | // args: ReportAccumulatorArgs; 56 | } 57 | 58 | interface ReportAccumulatorOfType { 59 | kind: TypeKind; 60 | name: string | null; 61 | ofType: ReportAccumulatorOfType | null; 62 | } 63 | 64 | interface Report { 65 | data: { 66 | types: ReportType[]; 67 | }; 68 | } 69 | 70 | interface ReportType { 71 | name: string; 72 | fields: ReportField[]; 73 | } 74 | 75 | interface ReportField { 76 | type: string; 77 | parentType: string; 78 | name: string; 79 | occurrences: ReportOccurrence[]; 80 | } 81 | 82 | interface ReportOccurrence { 83 | filename: string; 84 | rootNodeName: string; 85 | } 86 | 87 | function buildInitialState(schema: GraphQLSchema): ReportAccumulator { 88 | const types = schema.toConfig().types.reduce((typeMap, type) => { 89 | // TODO: non-object types 90 | return isObjectType(type) 91 | ? { 92 | ...typeMap, 93 | [type.name]: buildType(type) 94 | } 95 | : typeMap; 96 | }, {}); 97 | 98 | return { 99 | types 100 | }; 101 | } 102 | 103 | function buildType(type: GraphQLNamedType): ReportAccumulatorType { 104 | if (isObjectType(type)) return buildObjectType(type); 105 | 106 | throw new Error("report: buildType expects a GraphQLNamedType"); 107 | } 108 | 109 | function buildObjectType(type: GraphQLObjectType): ReportAccumulatorObjectType { 110 | return { 111 | fields: buildFields(type), 112 | // kind: TypeKind.Object, 113 | name: type.name 114 | }; 115 | } 116 | 117 | function buildFields(type: GraphQLObjectType): ReportAccumulatorFieldMap { 118 | return Object.values(type.getFields()).reduce((fieldMap, currentField) => { 119 | return { 120 | ...fieldMap, 121 | [currentField.name]: buildField(currentField) 122 | }; 123 | }, {}); 124 | } 125 | 126 | function buildField(field: GraphQLField): ReportAccumulatorField { 127 | const { type } = field; 128 | return { 129 | name: field.name, 130 | occurrences: [], 131 | type: buildOfType(type) 132 | }; 133 | } 134 | 135 | function getTypeKind(type: GraphQLType): TypeKind { 136 | if (isScalarType(type)) return TypeKind.Scalar; 137 | if (isObjectType(type)) return TypeKind.Object; 138 | if (isInterfaceType(type)) return TypeKind.Interface; 139 | if (isUnionType(type)) return TypeKind.Union; 140 | if (isEnumType(type)) return TypeKind.Enum; 141 | if (isListType(type)) return TypeKind.List; 142 | if (isNonNullType(type)) return TypeKind.NonNull; 143 | // TODO: add input types? 144 | 145 | throw new Error("report: getTypeKind expects a GraphQLType"); 146 | } 147 | 148 | function buildOfType(type: GraphQLType): ReportAccumulatorOfType { 149 | return { 150 | kind: getTypeKind(type), 151 | name: isNamedType(type) ? type.name : null, 152 | ofType: isWrappingType(type) ? buildOfType(type.ofType) : null 153 | }; 154 | } 155 | 156 | function addOccurrence( 157 | state: ReportAccumulator, 158 | typeName: string, 159 | fieldName: string, 160 | occurrence: ReportOccurrence 161 | ): ReportAccumulator { 162 | // TODO: handle non-object types 163 | if (!state.types[typeName]) { 164 | return state; 165 | } 166 | 167 | // TODO: handle invalid type/field names. 168 | // TODO: why doesn't TS warn about potentially null values here? 169 | state.types[typeName].fields[fieldName].occurrences.push(occurrence); 170 | return state; 171 | } 172 | 173 | function format(report: ReportAccumulator): Report { 174 | const sortByName = R.sortBy(R.prop("name")); 175 | 176 | const types = Object.values(report.types).map(type => { 177 | // TODO: Remove this. Types are formatted as strings and the parent type is here for ease of 178 | // refactoring the FE and integration tests. 179 | const fields = Object.values(type.fields).map(field => { 180 | return { 181 | ...field, 182 | type: formatOfType(field.type), 183 | parentType: type.name 184 | }; 185 | }); 186 | 187 | return { 188 | ...type, 189 | fields: sortByName(fields) 190 | }; 191 | }); 192 | 193 | return { data: { types: sortByName(types) } }; 194 | } 195 | 196 | function formatOfType(ofType: ReportAccumulatorOfType | null): string { 197 | if (ofType && ofType.kind === TypeKind.NonNull) { 198 | return `${formatOfType(ofType.ofType)}!`; 199 | } 200 | 201 | if (ofType && ofType.kind === TypeKind.List) { 202 | return `[${formatOfType(ofType.ofType)}]`; 203 | } 204 | 205 | if (!ofType || !ofType.name) { 206 | throw new Error( 207 | "report: formatOfType expects ofType to be a wrapper or named type" 208 | ); 209 | } 210 | 211 | return ofType.name; 212 | } 213 | 214 | export { addOccurrence, buildInitialState, format, Report }; 215 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | 4 | import { Report } from "./report"; 5 | 6 | function createServer(stats: Report) { 7 | const app = express(); 8 | 9 | app.use(express.static(path.resolve(__dirname, "../build"))); 10 | 11 | app.get("/stats", (_req, res) => { 12 | res.json(stats); 13 | }); 14 | 15 | return app; 16 | } 17 | 18 | export default createServer; 19 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/facebook/relay/blob/master/packages/relay-compiler/language/RelayLanguagePluginInterface.js 2 | 3 | export interface GraphQLTag { 4 | /** 5 | * Should hold the string content of the `graphql` tagged template literal, 6 | * which is either an operation or fragment. 7 | * 8 | * @example 9 | * 10 | * grapqhl`query MyQuery { … }` 11 | * grapqhl`fragment MyFragment on MyType { … }` 12 | */ 13 | template: string; 14 | 15 | /** 16 | * The location in the source file that the tag is placed at. 17 | */ 18 | sourceLocationOffset: { 19 | /** 20 | * The line in the source file that the tag is placed on. 21 | * 22 | * Lines use 1-based indexing. 23 | */ 24 | line: number; 25 | 26 | /** 27 | * The column in the source file that the tag starts on. 28 | * 29 | * Columns use 1-based indexing. 30 | */ 31 | column: number; 32 | }; 33 | 34 | /** 35 | * Absolute path to the local source file 36 | */ 37 | filePath: string; 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | // Work around for Jest and Mocha declaring the same global types. 9 | // Mocha is a dependency of @oclif/test. 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "target": "es2017", 13 | "composite": true, 14 | "moduleResolution": "node", 15 | "esModuleInterop": true 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@oclif/tslint", "tslint-config-prettier"], 3 | "rules": { 4 | "file-name-casing": [true, { ".tsx": "pascal-case", ".ts": "camel-case" }] 5 | } 6 | } 7 | --------------------------------------------------------------------------------