├── .nvmrc ├── src ├── index.js ├── etc.js └── microfiber.js ├── static ├── anvil.png └── microfiber.png ├── test ├── mocha-environment.js ├── mocha-setup.js ├── mocha-config.js ├── test-helpers │ └── index.js ├── .eslintrc.js └── index.test.js ├── index.js ├── nodemon.json ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .eslintrc.js ├── CHANGELOG.md ├── babel.config.js ├── LICENSE.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './microfiber' 2 | export * from './etc' 3 | -------------------------------------------------------------------------------- /static/anvil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilco/graphql-introspection-tools/HEAD/static/anvil.png -------------------------------------------------------------------------------- /static/microfiber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilco/graphql-introspection-tools/HEAD/static/microfiber.png -------------------------------------------------------------------------------- /test/mocha-environment.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | 3 | global.chai = chai 4 | global.expect = chai.expect 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This file is mainly just for cleaning up the exports to be intuitive/commonjs 2 | module.exports = require('./dist') 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": 1000, 3 | "verbose": true, 4 | "ext": "js,json,gql,graphql,graphqls", 5 | "ignore": [ 6 | ".git" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Babel's output 2 | dist/ 3 | 4 | yarn-error.log 5 | 6 | *.DS_Store 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Output of 'npm pack' 12 | *.tgz 13 | -------------------------------------------------------------------------------- /test/mocha-setup.js: -------------------------------------------------------------------------------- 1 | import { get } from 'bdd-lazy-var/getter' 2 | 3 | // https://github.com/stalniy/bdd-lazy-var/issues/56#issuecomment-639248242 4 | // eslint-disable-next-line id-length 5 | global.$ = get 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - newhouse 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | sourceType: 'module' 12 | }, 13 | plugins: [ 14 | ], 15 | rules: { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 2.1.1 2 | - Update dependencies 3 | 4 | #### 2.1.0 5 | - Fix/Add support for `EnumValue` via `getEnumValue()` and `getField()` 6 | - Update dependencies 7 | 8 | #### 2.0.2 9 | - Update dependencies 10 | 11 | #### 2.0.1 12 | - Update dependencies 13 | 14 | #### 2.0.0 15 | - Drop support for Node `< 14` 16 | - Add support for Node `18` 17 | - Update dependencies -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | // Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set. 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | // Keep this roughly in-line with our "engines.node" value in package.json 9 | node: '14' 10 | } 11 | } 12 | ], 13 | ], 14 | plugins: [ 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/mocha-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | diff: true, 3 | delay: false, 4 | extension: ['js'], 5 | package: './package.json', 6 | reporter: 'spec', 7 | slow: 75, 8 | timeout: 2000, 9 | spec: [ 10 | './test/**/*.test.js', 11 | ], 12 | require: [ 13 | // https://mochajs.org/#-require-module-r-module 14 | '@babel/register', 15 | './test/mocha-environment.js', 16 | ], 17 | ui: 'bdd-lazy-var/getter', 18 | file: './test/mocha-setup.js', 19 | exit: true, 20 | } 21 | -------------------------------------------------------------------------------- /test/test-helpers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | buildSchema, 3 | getIntrospectionQuery, 4 | graphqlSync, 5 | } from 'graphql' 6 | 7 | 8 | export const introspectionResponseFromSchemaSDL = ({ schemaSDL }) => { 9 | return introspectionResponseFromSchema({ 10 | schema: buildSchema(schemaSDL), 11 | }) 12 | } 13 | 14 | function introspectionResponseFromSchema ({ schema }) { 15 | return standardizeIntrospectionQueryResult( 16 | graphqlSync({ schema, source: getIntrospectionQuery() }) 17 | ) 18 | } 19 | 20 | // Get rid of the `data` envelope 21 | function standardizeIntrospectionQueryResult (result) { 22 | return result.data ? result.data : result 23 | } 24 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.js', 3 | env: { 4 | mocha: true 5 | }, 6 | plugins: [ 7 | 'mocha', 8 | ], 9 | rules: { 10 | 'mocha/no-skipped-tests': 'error', 11 | 'mocha/no-exclusive-tests': 'error', 12 | }, 13 | globals: { 14 | expect: 'readonly', 15 | pathToProject: 'readonly', 16 | // ************************************************* 17 | // bdd-lazy-var 18 | // 19 | // In order to get around eslint complaining for now: 20 | // https://github.com/stalniy/bdd-lazy-var/issues/56#issuecomment-639248242 21 | // eslint-disable-next-line id-length 22 | $: 'readonly', 23 | def: 'readonly', 24 | // 25 | // ************************************************* 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/etc.js: -------------------------------------------------------------------------------- 1 | // GraphQL constants 2 | const KIND_SCALAR = 'SCALAR' 3 | const KIND_OBJECT = 'OBJECT' 4 | const KIND_INTERFACE = 'INTERFACE' 5 | const KIND_UNION = 'UNION' 6 | const KIND_ENUM = 'ENUM' 7 | const KIND_INPUT_OBJECT = 'INPUT_OBJECT' 8 | const KIND_LIST = 'LIST' 9 | const KIND_NON_NULL = 'NON_NULL' 10 | 11 | // An Object containing all the GraphQL Kind values you may encounter. 12 | export const KINDS = Object.freeze({ 13 | SCALAR: KIND_SCALAR, 14 | OBJECT: KIND_OBJECT, 15 | INTERFACE: KIND_INTERFACE, 16 | UNION: KIND_UNION, 17 | ENUM: KIND_ENUM, 18 | INPUT_OBJECT: KIND_INPUT_OBJECT, 19 | LIST: KIND_LIST, 20 | NON_NULL: KIND_NON_NULL, 21 | }) 22 | 23 | // A function that compares 2 types and determines if they have the same Kind and Name. 24 | export function typesAreSame (typeA, typeB) { 25 | return typeA.kind === typeB.kind && typeA.name === typeB.name 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Anvil Foundry Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI Tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | env: 13 | GITHUB_SHA: ${{ github.event.pull_request.head.sha }} 14 | 15 | jobs: 16 | build-and-cache-dist: 17 | name: Build and Cache dist 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Setup Node.js 24 | id: setup-node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version-file: '.nvmrc' 28 | cache: 'yarn' 29 | 30 | - name: Cache dist after Build 31 | uses: actions/cache@v3 32 | with: 33 | path: dist/ 34 | key: ${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ env.GITHUB_SHA }} 35 | 36 | - run: yarn install 37 | - run: yarn build 38 | 39 | test: 40 | needs: build-and-cache-dist 41 | runs-on: ubuntu-latest 42 | strategy: 43 | matrix: 44 | node-version: [14, 16, 18] 45 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 46 | steps: 47 | - uses: actions/checkout@v3 48 | 49 | - name: Load dist from cache 50 | uses: actions/cache@v3 51 | with: 52 | path: dist/ 53 | key: ${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ env.GITHUB_SHA }} 54 | 55 | - name: Use Node.js ${{ matrix.node-version }} 56 | uses: actions/setup-node@v3 57 | with: 58 | node-version: ${{ matrix.node-version }} 59 | cache: 'yarn' 60 | - run: yarn install 61 | - run: yarn test 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anvilco/graphql-introspection-tools", 3 | "version": "2.1.1", 4 | "description": "A library to query and manipulate GraphQL Introspection Query results in some useful ways.", 5 | "author": "Chris Newhouse", 6 | "homepage": "https://github.com/anvilco/graphql-introspection-tools", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/anvilco/graphql-introspection-tools.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/anvilco/graphql-introspection-tools/issues" 14 | }, 15 | "keywords": [ 16 | "graphql", 17 | "schema", 18 | "introspection", 19 | "introspection-query", 20 | "introspection-query-manipulator", 21 | "query", 22 | "remove", 23 | "manipulate", 24 | "manipulator" 25 | ], 26 | "main": "index.js", 27 | "files": [ 28 | "package.json", 29 | "README.md", 30 | "LICENSE.md", 31 | "CHANGELOG.md", 32 | "dist/" 33 | ], 34 | "engines": { 35 | "node": ">=14" 36 | }, 37 | "scripts": { 38 | "build": "babel src --out-dir ./dist", 39 | "clean": "yarn rimraf ./dist", 40 | "prepare": "yarn clean && yarn build", 41 | "pub": "yarn prepare && npm publish", 42 | "pub:dry-run": "yarn prepare && npm publish --dry-run", 43 | "test": "yarn prepare && yarn mocha --config ./test/mocha-config.js", 44 | "test:watch": "nodemon -x 'yarn test' --ignore dist/", 45 | "test:debug:watch": "nodemon -x 'yarn test:debug' --ignore dist/", 46 | "test:debug": "yarn test --node-option inspect=0.0.0.0:9223" 47 | }, 48 | "dependencies": { 49 | "lodash.defaults": "^4.2.0", 50 | "lodash.get": "^4.4.2", 51 | "lodash.unset": "^4.5.2" 52 | }, 53 | "devDependencies": { 54 | "@babel/cli": "^7.17.6", 55 | "@babel/core": "^7.17.8", 56 | "@babel/preset-env": "^7.16.11", 57 | "@babel/register": "^7.17.0", 58 | "bdd-lazy-var": "^2.6.1", 59 | "chai": "^4.3.6", 60 | "eslint": "^8.10.0", 61 | "eslint-plugin-mocha": "^10.0.3", 62 | "graphql": "^16.3.0", 63 | "lodash.isequal": "^4.5.0", 64 | "mocha": "^10.2.0", 65 | "nodemon": "^3.0.1", 66 | "rimraf": "^5.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Microfiber - A.K.A. GraphQL Introspection Tools 4 | 5 | [![npm][npm]][npm-url] 6 | [![downloads][npm-downloads]][npm-url] 7 | 8 | 9 | 10 | A library to query and manipulate GraphQL Introspection Query results in some useful ways. What ways you ask? 11 | 12 | How about: 13 | - Digging through your Introspection Query Results for a specific Query, Mutation, Type, Field, Argument or Subscription. 14 | - Removing a specific Query, Mutation, Type, Field/InputField, Argument or Subscription from your Introspection Query Results. 15 | - Removing Queries, Mutations, Fields/InputFields or Arguments that refer to Type that does not exist in - or has been removed from - your Introspection Query Results. 16 | 17 | Yay! 18 | 19 | It's called `microfiber` because it is heavily used to do the cleaning and manipulation in [SpectaQL][spectaql]...it *cleans* the *spectacles*, get it?! 20 | 21 | But, we also wanted to have a more intuitive, literal name so that people could find it. Hence it's also known as `@anvilco/graphql-introspection-tools`. 22 | 23 | --- 24 | 25 | **Repository sponsored by [Anvil](www.useanvil.com/developers)** 26 | 27 | ![Horizontal Lockupblack](https://user-images.githubusercontent.com/293079/169453889-ae211c6c-7634-4ccd-8ca9-8970c2621b6f.png#gh-light-mode-only) 28 | ![Horizontal Lockup copywhite](https://user-images.githubusercontent.com/293079/169453892-895f637b-4633-4a14-b997-960c9e17579b.png#gh-dark-mode-only) 29 | 30 | Anvil provides easy APIs for all things paperwork. 31 | 32 | 1. [PDF filling API](https://www.useanvil.com/products/pdf-filling-api/) - fill out a PDF template with a web request and structured JSON data. 33 | 2. [PDF generation API](https://www.useanvil.com/products/pdf-generation-api/) - send markdown or HTML and Anvil will render it to a PDF. 34 | 3. [Etch e-sign with API](https://www.useanvil.com/products/etch/) - customizable, embeddable, e-signature platform with an API to control the signing process end-to-end. 35 | 4. [Anvil Workflows (w/ API)](https://www.useanvil.com/products/workflows/) - Webforms + PDF + e-sign with a powerful no-code builder. Easily collect structured data, generate PDFs, and request signatures. 36 | 37 | Learn more on our [Anvil developer page](https://www.useanvil.com/developers/). 38 | 39 | --- 40 | 41 | ## Getting Started 42 | 43 | 1. Install `microfiber` 44 | ```sh 45 | npm install microfiber 46 | # OR 47 | yarn add microfiber 48 | ``` 49 | 50 | 2. Clean your GraphQL Introspection Query Results 51 | ```node 52 | import { Microfiber } from 'microfiber' 53 | 54 | const introspectionQueryResults = {...} 55 | 56 | const microfiber = new Microfiber(introspectionQueryResults) 57 | 58 | // ...do some things to your schema with `microfiber` 59 | 60 | const cleanedIntrospectonQueryResults = microfiber.getResponse() 61 | 62 | // ...do something with your cleaned Introspection Query Results. 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### class Microfiber 68 | 69 | Most of the useful stuff in this library is done through creating a new Microfiber class instance with your Introspection Query Results, and querying or manipulating it via that instance. Here are most of the interesting bits to know about class behavior. 70 | 71 | --- 72 | #### constructor 73 | ```node 74 | const introspectionQueryResponse = {...} 75 | // Here are the publicly supported options and their sane defaults: 76 | const options = { 77 | // Some GraphQL implementations have non-standard Query, Mutation and/or Subscription 78 | // type names. This option will fix them if they're messed up in the Introspection Query 79 | // Results 80 | fixQueryAndMutationAndSubscriptionTypes: true, 81 | 82 | // Remove Types that are not referenced anywhere by anything 83 | removeUnusedTypes: true, 84 | 85 | // Remove things whose Types are not found due to being removed 86 | removeFieldsWithMissingTypes: true, 87 | removeArgsWithMissingTypes: true, 88 | removeInputFieldsWithMissingTypes: true, 89 | removePossibleTypesOfMissingTypes: true, 90 | 91 | // Remove all the types and things that are unreferenced immediately? 92 | cleanupSchemaImmediately: true, 93 | } 94 | 95 | const microfiber = new Microfiber(introspectionQueryResponse, options) 96 | ``` 97 | --- 98 | #### cleanSchema 99 | Clean up the schema by removing: 100 | - Fields or Input Fields whose Type does not exist in the schema. 101 | - Args whose Type does not exist in the schema. 102 | - Possible Types in a Union that do not exist in the schema. 103 | - Queries or Mutations whose return Type does not exist in the schema. 104 | 105 | This method is usually called after altering the schema in any way so as to not leave any dangling/orphaned things around the schema. 106 | ```node 107 | microfiber.cleanSchema() 108 | ``` 109 | --- 110 | #### getResponse 111 | Get out the Introspection Query Result that you have manipulated with Microfiber as an Object. 112 | ```node 113 | const cleanedResponse = microfiber.getResponse() 114 | ``` 115 | --- 116 | #### getAllTypes 117 | Get all the Types from your schema as an Array of Objects. Supported options and their sane defaults are shown. 118 | ```node 119 | const allTypes = microfiber.getAllTypes({ 120 | // Include reserved GraphQL types? 121 | includeReserved: false, 122 | // Include the Query type? 123 | includeQuery: false, 124 | // Include the Mutation type? 125 | includeMutation: false, 126 | // Include the Subscription type? 127 | includeSubscription: false, 128 | } = {}) 129 | ``` 130 | --- 131 | #### getType 132 | Get a specific Type from your schema. Supported params and their sane defaults are shown. 133 | ```node 134 | const type = microfiber.getType({ kind: 'OBJECT', name }) 135 | ``` 136 | --- 137 | #### getDirectives 138 | Get all the Directives from your schema. 139 | ```node 140 | const directives = microfiber.getDirectives() 141 | ``` 142 | --- 143 | #### getDirective 144 | Get a specific Directive from your schema. Supported params and their sane defaults are shown. 145 | ```node 146 | const directive = microfiber.getDirective({ name }) 147 | ``` 148 | --- 149 | #### getQueryType 150 | Get the Query Type from your schema. 151 | ```node 152 | const queryType = microfiber.getQueryType() 153 | ``` 154 | --- 155 | #### getQuery 156 | Get a specific Query from your schema. 157 | ```node 158 | const query = microfiber.getQuery({ name }) 159 | ``` 160 | --- 161 | #### getMutationType 162 | Get the Mutation Type from your schema. 163 | ```node 164 | const mutationType = microfiber.getMutationType() 165 | ``` 166 | --- 167 | #### getMutation 168 | Get a specific Mutation from your schema. 169 | ```node 170 | const mutation = microfiber.getMutation({ name }) 171 | ``` 172 | --- 173 | #### getSubscriptionType 174 | Get the Subscription Type from your schema. 175 | ```node 176 | const subscriptionType = microfiber.getSubscription() 177 | ``` 178 | --- 179 | #### getSubscription 180 | Get a specific Subscription from your schema. 181 | ```node 182 | const subscription = microfiber.getSubscription({ name }) 183 | ``` 184 | --- 185 | #### getField 186 | Get a specific Field from your schema. Supported params and their sane defaults are shown. 187 | ```node 188 | const field = microfiber.getField({ typeKind: 'OBJECT', typeName, fieldName }) 189 | ``` 190 | --- 191 | #### getInterfaceField 192 | Get a specific Field from an Interface in your schema. A convenience wrapper around `getField({ typeKind: 'INTERFACE', ...})` 193 | ```node 194 | const interfaceField = microfiber.getInterfaceField({ typeName, fieldName }) 195 | ``` 196 | --- 197 | #### getEnumValue 198 | Get a specific EnumValue from your schema. A convenience wrapper around `getField({ typeKind: 'ENUM', ...})` 199 | ```node 200 | const inputField = microfiber.getEnumValue({ typeName, fieldName }) 201 | ``` 202 | --- 203 | #### getInputField 204 | Get a specific InputField from your schema. A convenience wrapper around `getField({ typeKind: 'INPUT_OBJECT', ...})` 205 | ```node 206 | const inputField = microfiber.getInputField({ typeName, fieldName }) 207 | ``` 208 | --- 209 | #### getArg 210 | Get a specific Arg from your schema. Supported params and their sane defaults are shown. 211 | ```node 212 | const arg = microfiber.getArg({ typeKind: 'OBJECT', typeName, fieldName, argName }) 213 | ``` 214 | --- 215 | #### getDirectiveArg 216 | Get a specific Arg from a specifig Directive in your schema. Supported params and their sane defaults are shown. 217 | ```node 218 | const directiveArg = microfiber.getDirectiveArg({ directiveName, argName }) 219 | ``` 220 | --- 221 | #### removeDirective 222 | Get a specific Directive from your schema. Supported params and their sane defaults are shown. 223 | ```node 224 | const directiveArg = microfiber.removeDirective({ 225 | name, 226 | // Clean up the schema afterwards? 227 | cleanup = true, 228 | }) 229 | ``` 230 | --- 231 | #### removeType 232 | Remove a Type from your schema, and optionally the references to that Type elsewhere in your schema. Supported params and their sane defaults are shown. 233 | ```node 234 | microfiber.removeType({ 235 | kind: 'OBJECT', 236 | name, 237 | // Clean up the schema afterwards? 238 | cleanup: true, 239 | // Remove occurances of this Type from other places? 240 | removeFieldsOfType: constructorOptions.removeFieldsWithMissingTypes, 241 | removeInputFieldsOfType: constructorOptions.removeInputFieldsWithMissingTypes, 242 | removePossibleTypesOfType: constructorOptions.removePossibleTypesOfMissingTypes, 243 | removeArgsOfType: constructorOptions.removeArgsWithMissingTypes, 244 | }) 245 | ``` 246 | --- 247 | #### removeField 248 | Remove a specific Field from a specific Type in your schema. Supported params and their sane defaults are shown. 249 | ```node 250 | microfiber.removeField({ 251 | typeKind: 'OBJECT', 252 | typeName, 253 | fieldName, 254 | // Clean up the schema afterwards? 255 | cleanup: true, 256 | }) 257 | ``` 258 | --- 259 | #### removeInputField 260 | Remove a specific Input Field from a specific Input Object in your schema. Supported params and their sane defaults are shown. 261 | ```node 262 | microfiber.removeInputField({ 263 | typeName, 264 | fieldName, 265 | // Clean up the schema afterwards? 266 | cleanup: true, 267 | }) 268 | ``` 269 | --- 270 | #### removeArg 271 | Remove a specific Arg from a specific Field or Input Field in your schema. Supported params and their sane defaults are shown. 272 | ```node 273 | microfiber.removeArg({ 274 | typeKind, 275 | typeName, 276 | fieldName, 277 | argName, 278 | // Clean up the schema afterwards? 279 | cleanup: true, 280 | }) 281 | ``` 282 | --- 283 | #### removeEnumValue 284 | Remove a specifc Enum value from an Enum Type in your schema. Supported params are shown. 285 | ```node 286 | microfiber.removeEnumValue({ 287 | // The name of the Enum Type 288 | name, 289 | // The Enum value you want to remove 290 | value, 291 | }) 292 | ``` 293 | --- 294 | #### removePossibleType 295 | Remove a Possible Type from a specific Union Type in your schema. Supported params and sane defaults are shown. 296 | ```node 297 | microfiber.removePossibleType({ 298 | // The name of the Union Type 299 | typeName, 300 | // The Kind of the possible Type you want to remove 301 | possibleTypeKind, 302 | // The name of the possible Type you want to remove 303 | possibleTypeName, 304 | // Clean up the schema afterwards? 305 | cleanup: true, 306 | }) 307 | ``` 308 | --- 309 | #### removeQuery 310 | Remove a specific Query from your schema. Supported params and their sane defaults are shown. 311 | ```node 312 | microfiber.removeQuery({ 313 | name, 314 | // Clean up the schema afterwards? 315 | cleanup: true, 316 | }) 317 | ``` 318 | --- 319 | #### removeMutation 320 | Remove a specific Mutation from your schema. Supported params and their sane defaults are shown. 321 | ```node 322 | microfiber.removeMutation({ 323 | name, 324 | // Clean up the schema afterwards? 325 | cleanup: true, 326 | }) 327 | ``` 328 | --- 329 | #### removeSubscription 330 | Remove a specific Subscription from your schema. Supported params and their sane defaults are shown. 331 | ```node 332 | microfiber.removeSubscription({ 333 | name, 334 | // Clean up the schema afterwards? 335 | cleanup: true, 336 | }) 337 | ``` 338 | 339 | ### Other exports from this library 340 | There are some other exports from this library, not just the `Microfiber` class. 341 | 342 | --- 343 | #### KINDS 344 | An Object containing all the GraphQL Kind values you may encounter. 345 | ```node 346 | import { KINDS } from 'microfiber' 347 | 348 | console.log(KINDS) 349 | 350 | // { 351 | // SCALAR: 'SCALAR', 352 | // OBJECT: 'OBJECT', 353 | // INTERFACE: 'INTERFACE', 354 | // UNION: 'UNION', 355 | // ENUM: 'ENUM', 356 | // INPUT_OBJECT: 'INPUT_OBJECT', 357 | // LIST: 'LIST', 358 | // NON_NULL: 'NON_NULL' 359 | // } 360 | ``` 361 | --- 362 | #### typesAreSame 363 | A function that compares 2 types and determines if they have the same Kind and Name. 364 | ```node 365 | import { typesAreSame } from 'microfiber' 366 | 367 | const typeA = { kind: 'OBJECT', name: 'Foo' } 368 | const typeB = { kind: 'OBJECT', name: 'Bar' } 369 | 370 | typesAreSame(typeA, typeB) // false 371 | typesAreSame(typeA, typeA) // true 372 | ``` 373 | --- 374 | #### digUnderlyingType 375 | A function that digs through any Non-Null and List nesting and returns the underlying Type. 376 | ```node 377 | import { digUnderlyingType } from 'microfiber' 378 | 379 | const nonNullableString = { 380 | name: null, 381 | kind: 'NON_NULL', 382 | ofType: { 383 | name: null, 384 | kind: 'LIST', 385 | ofType: { 386 | name: 'String', 387 | kind: 'SCALAR', 388 | } 389 | } 390 | } 391 | 392 | digUnderlyingType(nonNullableString) // { name: 'String', kind: 'SCALAR' } 393 | ``` 394 | --- 395 | #### isReservedType 396 | A function that returns a Boolean indicating whether a Type is special GraphQL reserved Type. 397 | ```node 398 | import { isReservedType } from 'microfiber' 399 | 400 | const myType = { name: 'Foo', ... } 401 | const reservedType = { name: '__Foo', ... } 402 | 403 | isReservedType(myType) // false 404 | isReservedType(reservedType) // true 405 | ``` 406 | 407 | [npm]: https://badge.fury.io/js/microfiber.svg 408 | [npm-downloads]: https://img.shields.io/npm/dw/microfiber 409 | [npm-url]: https://www.npmjs.com/package/microfiber 410 | [spectaql]: https://github.com/anvilco/spectaql 411 | -------------------------------------------------------------------------------- /src/microfiber.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash.get' 2 | import unset from 'lodash.unset' 3 | import defaults from 'lodash.defaults' 4 | 5 | import { 6 | KINDS, 7 | typesAreSame, 8 | } from './etc' 9 | 10 | 11 | // TODO: 12 | // 13 | // remove types that have no fields/inputFields/possibleTypes 14 | // 15 | // optimize to only clean if "dirty" and when pulling schema out 16 | 17 | const defaultOpts = Object.freeze({ 18 | // Perform an analysis of the schema right away. 19 | _analyze: true, 20 | // Perform some normalization of the Introspection Query Results 21 | _normalize: true, 22 | 23 | // Some GraphQL implementations have non-standard Query, Mutation and/or Subscription 24 | // type names. This option will fix them if they're messed up in the Introspection Query 25 | // Results 26 | fixQueryAndMutationAndSubscriptionTypes: true, 27 | 28 | // Remove Types that are not referenced anywhere by anything 29 | removeUnusedTypes: true, 30 | 31 | // Remove things whose Types are not found due to being removed 32 | removeFieldsWithMissingTypes: true, 33 | removeArgsWithMissingTypes: true, 34 | removeInputFieldsWithMissingTypes: true, 35 | removePossibleTypesOfMissingTypes: true, 36 | removeDirectiveArgumentsOfMissingTypes: true, 37 | 38 | // TODO: implement 39 | // removeQueriesWithMissingTypes: true, 40 | // TODO: implement 41 | // removeMutationsWithMissingTypes: true, 42 | // TODO: implement 43 | // removeSubscriptionsWithMissingTypes: true, 44 | 45 | // Remove all the types and things that are unreferenced immediately? 46 | cleanupSchemaImmediately: true, 47 | }) 48 | 49 | // Map some opts to their corresponding removeType params for proper defaulting 50 | const optsToRemoveTypeParamsMap = Object.freeze({ 51 | removeFieldsWithMissingTypes: 'removeFieldsOfType', 52 | removeArgsWithMissingTypes: 'removeArgsOfType', 53 | removeInputFieldsWithMissingTypes: 'removeInputFieldsOfType', 54 | removePossibleTypesOfMissingTypes: 'removePossibleTypesOfType', 55 | removeDirectiveArgumentsOfMissingTypes: 'removeDirectiveArgumentsOfType', 56 | }) 57 | 58 | 59 | const kindToFieldPropertyMap = Object.freeze({ 60 | [KINDS.OBJECT]: 'fields', 61 | [KINDS.INTERFACE]: 'fields', 62 | [KINDS.INPUT_OBJECT]: 'inputFields', 63 | [KINDS.ENUM]: 'enumValues', 64 | }) 65 | 66 | export class Microfiber { 67 | constructor(response, opts = {}) { 68 | if (!response) { 69 | throw new Error('No response provided!') 70 | } 71 | 72 | opts = defaults({}, opts, defaultOpts) 73 | this.setOpts(opts) 74 | 75 | // The rest of the initialization can be handled by this public method 76 | this.setResponse(response) 77 | } 78 | 79 | setOpts(opts) { 80 | this.opts = opts || {} 81 | } 82 | 83 | // Set/change the response on the instance 84 | setResponse(responseIn) { 85 | const response = JSON.parse(JSON.stringify(responseIn)) 86 | 87 | if (this.opts._normalize) { 88 | const normalizedResponse = Microfiber.normalizeIntrospectionResponse(response) 89 | if (normalizedResponse !== response) { 90 | this._wasNormalized = true 91 | } 92 | this.schema = get(normalizedResponse, '__schema') 93 | } else { 94 | this.schema = response 95 | } 96 | 97 | if (this.opts.fixQueryAndMutationAndSubscriptionTypes) { 98 | this._fixQueryAndMutationAndSubscriptionTypes() 99 | } 100 | 101 | // OK, time to validate 102 | this._validate() 103 | 104 | if (this.opts._analyze) { 105 | this._analyze() 106 | } 107 | 108 | if (this.opts.cleanupSchemaImmediately) { 109 | this.cleanSchema() 110 | } 111 | } 112 | 113 | // This is how you get OUT what you've put in and manipulated 114 | getResponse() { 115 | const clonedResponse = { 116 | __schema: this._cloneSchema() 117 | } 118 | 119 | if (this._wasNormalized) { 120 | return { 121 | data: clonedResponse, 122 | } 123 | } 124 | 125 | return clonedResponse 126 | } 127 | 128 | static normalizeIntrospectionResponse(response) { 129 | if (response && response.data) { 130 | return response.data 131 | } 132 | 133 | return response 134 | } 135 | 136 | static digUnderlyingType(type) { 137 | return digUnderlyingType(type) 138 | } 139 | 140 | getAllTypes({ 141 | // Include reserved GraphQL types? 142 | includeReserved = false, 143 | // Include the Query type? 144 | includeQuery = false, 145 | // Include the Mutation type? 146 | includeMutation = false, 147 | // Include the Subscription type? 148 | includeSubscription = false, 149 | } = {}) { 150 | const queryType = this.getQueryType() 151 | const mutationType = this.getMutationType() 152 | const subscriptionType = this.getSubscriptionType() 153 | 154 | return this.schema.types.filter((type) => { 155 | if (!includeReserved && isReservedType(type)) { 156 | return false 157 | } 158 | if (queryType && !includeQuery && typesAreSame(type, queryType)) { 159 | return false 160 | } 161 | if (mutationType && !includeMutation && typesAreSame(type, mutationType)) { 162 | return false 163 | } 164 | 165 | if (subscriptionType && !includeSubscription && typesAreSame(type, subscriptionType)) { 166 | return false 167 | } 168 | 169 | return true 170 | }) 171 | } 172 | 173 | getType({ kind = KINDS.OBJECT, name }) { 174 | return this.schema.types[this._getTypeIndex({ kind, name })] 175 | } 176 | 177 | getDirectives () { 178 | return this.schema.directives 179 | } 180 | 181 | getDirective({ name }) { 182 | if (!name) { 183 | return 184 | } 185 | 186 | return this.getDirectives()[this._getDirectiveIndex({ name })] 187 | } 188 | 189 | getQueryType() { 190 | if (!this.queryTypeName) { 191 | return false 192 | } 193 | 194 | return this.getType({ kind: KINDS.OBJECT, name: this.queryTypeName }) 195 | } 196 | 197 | getQuery({ name }) { 198 | const queryType = this.getQueryType() 199 | if (!queryType) { 200 | return false 201 | } 202 | 203 | return this.getField({ typeKind: queryType.kind, typeName: queryType.name, fieldName: name }) 204 | } 205 | 206 | getMutationType() { 207 | if (!this.mutationTypeName) { 208 | return false 209 | } 210 | 211 | return this.getType({ kind: KINDS.OBJECT, name: this.mutationTypeName }) 212 | } 213 | 214 | getMutation({ name }) { 215 | const mutationType = this.getMutationType() 216 | if (!mutationType) { 217 | return false 218 | } 219 | 220 | return this.getField({ typeKind: mutationType.kind, typeName: mutationType.name, fieldName: name }) 221 | } 222 | 223 | getSubscriptionType() { 224 | if (!this.subscriptionTypeName) { 225 | return false 226 | } 227 | 228 | return this.getType({ kind: KINDS.OBJECT, name: this.subscriptionTypeName }) 229 | } 230 | 231 | getSubscription({ name }) { 232 | const subscriptionType = this.getSubscriptionType() 233 | if (!subscriptionType) { 234 | return false 235 | } 236 | 237 | return this.getField({ typeKind: subscriptionType.kind, typeName: subscriptionType.name, fieldName: name }) 238 | } 239 | 240 | getField({ typeKind = KINDS.OBJECT, typeName, fieldName }) { 241 | const type = this.getType({ kind: typeKind, name: typeName }) 242 | if (!type) { 243 | return 244 | } 245 | const fieldsProperty = kindToFieldPropertyMap[typeKind] 246 | if (!(fieldsProperty && type[fieldsProperty])) { 247 | return 248 | } 249 | 250 | return type[fieldsProperty].find((field) => field.name === fieldName) 251 | } 252 | 253 | getEnumValue({ typeName, fieldName }) { 254 | return this.getField({ typeKind: KINDS.ENUM, typeName, fieldName }) 255 | } 256 | 257 | getInputField({ typeName, fieldName }) { 258 | return this.getField({ typeKind: KINDS.INPUT_OBJECT, typeName, fieldName }) 259 | } 260 | 261 | getInterfaceField({ typeName, fieldName }) { 262 | return this.getField({ typeKind: KINDS.INTERFACE, typeName, fieldName }) 263 | } 264 | 265 | getArg({ typeKind = KINDS.OBJECT, typeName, fieldName, argName }) { 266 | const field = this.getField({ typeKind, typeName, fieldName }) 267 | if (!(field && field.args.length)) { 268 | return 269 | } 270 | 271 | return field.args.find((arg) => arg.name === argName) 272 | } 273 | 274 | getDirectiveArg({ directiveName, argName }) { 275 | const directive = this.getDirective({ name: directiveName }) 276 | if (!(directive && directive.args.length)) { 277 | return 278 | } 279 | 280 | return directive.args.find((arg) => arg.name === argName) 281 | } 282 | 283 | removeDirective({ name, cleanup = true }) { 284 | if (!name) { 285 | return 286 | } 287 | this.schema.directives = this.schema.directives.filter((directive) => directive.name !== name) 288 | if (cleanup) { 289 | this.cleanSchema() 290 | } 291 | } 292 | 293 | removeType({ 294 | kind = KINDS.OBJECT, 295 | name, 296 | // Clean up the schema afterwards? 297 | cleanup = true, 298 | // Remove occurances of this Type from other places? 299 | removeFieldsOfType, 300 | removeInputFieldsOfType, 301 | removePossibleTypesOfType, 302 | removeArgsOfType, 303 | removeDirectiveArgumentsOfType, 304 | }) { 305 | const typeKey = buildKey({ kind, name }) 306 | if (!Object.prototype.hasOwnProperty.call(this.typeToIndexMap, typeKey)) { 307 | return false 308 | } 309 | const typeIndex = this.typeToIndexMap[typeKey] 310 | if (isUndef(typeIndex)) { 311 | return false 312 | } 313 | 314 | // Create an object of some of the opts, but mapped to keys that match the params 315 | // of this method. They will then be used as the default value for the params 316 | // so that constructor opts will be the default, but they can be overridden in 317 | // the call. 318 | const mappedOpts = mapProps({ props: this.opts, map: optsToRemoveTypeParamsMap }) 319 | const mergedOpts = defaults( 320 | { 321 | removeFieldsOfType, 322 | removeInputFieldsOfType, 323 | removePossibleTypesOfType, 324 | removeArgsOfType, 325 | removeDirectiveArgumentsOfType, 326 | }, 327 | mappedOpts, 328 | ) 329 | 330 | // If we are going to clean up afterwards, then the others should not have to 331 | const shouldOthersClean = !cleanup 332 | 333 | const originalSchema = this._cloneSchema() 334 | 335 | try { 336 | // If we are removing a special type like a Query or Mutation or Subscription 337 | // then there's some special stuff to do 338 | if (typesAreSame(this.getQueryType() || {}, { kind, name })) { 339 | delete this.queryTypeName 340 | delete this.schema.queryType 341 | } else if (typesAreSame(this.getMutationType() || {}, { kind, name })) { 342 | delete this.mutationTypeName 343 | delete this.schema.mutationType 344 | } else if (typesAreSame(this.getSubscriptionType() || {}, { kind, name })) { 345 | delete this.subscriptionTypeName 346 | delete this.schema.subscriptionType 347 | } 348 | 349 | // Do this *after* the special stuff above, if necessary 350 | delete this.schema.types[typeIndex] 351 | delete this.typeToIndexMap[typeKey] 352 | 353 | if (mergedOpts.removeArgsOfType) { 354 | this._removeArgumentsOfType({ kind, name, cleanup: shouldOthersClean }) 355 | } 356 | 357 | if (mergedOpts.removeFieldsOfType) { 358 | this._removeFieldsOfType({ kind, name, cleanup: shouldOthersClean }) 359 | } 360 | 361 | if (mergedOpts.removeInputFieldsOfType) { 362 | this._removeInputFieldsOfType({ kind, name, cleanup: shouldOthersClean }) 363 | } 364 | 365 | // AKA Unions 366 | if (mergedOpts.removePossibleTypesOfType) { 367 | this._removePossibleTypesOfType({ kind, name, cleanup: shouldOthersClean }) 368 | } 369 | 370 | if (mergedOpts.removeDirectiveArgumentsOfType) { 371 | this._removeDirectiveArgumentsOfType({ kind, name, cleanup: shouldOthersClean }) 372 | } 373 | 374 | if (cleanup) { 375 | this.cleanSchema() 376 | } 377 | 378 | return true 379 | } catch (err) { 380 | this.schema = originalSchema 381 | throw err 382 | } 383 | } 384 | 385 | removeField({ 386 | typeKind = KINDS.OBJECT, 387 | typeName, 388 | fieldName, 389 | // Clean up the schema afterwards? 390 | cleanup = true, 391 | }) { 392 | const type = this.getType({ kind: typeKind, name: typeName }) 393 | if (!type) { 394 | return false 395 | } 396 | 397 | const fieldsProperty = kindToFieldPropertyMap[typeKind] 398 | if (!(fieldsProperty && type[fieldsProperty])) { 399 | return false 400 | } 401 | 402 | // TODO: build a map for the locations of fields on types? 403 | type[fieldsProperty] = type[fieldsProperty].filter((field) => field.name !== fieldName) 404 | 405 | if (cleanup) { 406 | this.cleanSchema() 407 | } 408 | } 409 | 410 | removeInputField({ 411 | typeName, 412 | fieldName, 413 | // Clean up the schema afterwards? 414 | cleanup = true, 415 | }) { 416 | return this.removeField({ typeKind: KINDS.INPUT_OBJECT, typeName, fieldName, cleanup }) 417 | } 418 | 419 | removeArg({ 420 | typeKind, 421 | typeName, 422 | fieldName, 423 | argName, 424 | // Clean up the schema afterwards? 425 | cleanup = true, 426 | }) { 427 | const field = this.getField({ typeKind, typeName, fieldName }) 428 | // field.args should alwys be an array, never null 429 | if (!field) { 430 | return false 431 | } 432 | 433 | // TODO: build a map for the locations of args on fields? 434 | field.args = field.args.filter((arg) => arg.name !== argName) 435 | 436 | if (cleanup) { 437 | this.cleanSchema() 438 | } 439 | } 440 | 441 | // Remove just a single possible value for an Enum, but not the whole Enum 442 | removeEnumValue({ 443 | // The name of the Enum Type 444 | name, 445 | // The Enum value you want to remove 446 | value, 447 | }) { 448 | const type = this.getType({ kind: KINDS.ENUM, name }) 449 | if (!(type && type.enumValues)) { 450 | return false 451 | } 452 | 453 | type.enumValues = type.enumValues.filter((enumValue) => enumValue.name !== value) 454 | } 455 | 456 | removePossibleType({ 457 | // The name of the Union Type 458 | typeName, 459 | // The Kind of the possible Type you want to remove 460 | possibleTypeKind, 461 | // The name of the possible Type you want to remove 462 | possibleTypeName, 463 | // Clean up the schema afterwards? 464 | cleanup = true, 465 | }) { 466 | const type = this.getType({ kind: KINDS.UNION, name: typeName }) 467 | if (!(type && type.possibleTypes)) { 468 | return false 469 | } 470 | 471 | type.possibleTypes = type.possibleTypes.filter((possibleType) => possibleType.type !== possibleTypeKind && possibleType.name !== possibleTypeName) 472 | if (cleanup) { 473 | this.cleanSchema() 474 | } 475 | } 476 | 477 | removeQuery({ 478 | name, 479 | // Clean up the schema afterwards? 480 | cleanup = true, 481 | }) { 482 | if (!this.queryTypeName) { 483 | return false 484 | } 485 | 486 | this.removeField({ typeKind: KINDS.OBJECT, typeName: this.queryTypeName, fieldName: name, cleanup }) 487 | } 488 | 489 | removeMutation({ 490 | name, 491 | // Clean up the schema afterwards? 492 | cleanup = true, 493 | }) { 494 | if (!this.mutationTypeName) { 495 | return false 496 | } 497 | 498 | this.removeField({ typeKind: KINDS.OBJECT, typeName: this.mutationTypeName, fieldName: name, cleanup }) 499 | } 500 | 501 | removeSubscription({ 502 | name, 503 | // Clean up the schema afterwards? 504 | cleanup = true, 505 | }) { 506 | if (!this.subscriptionTypeName) { 507 | return false 508 | } 509 | 510 | this.removeField({ typeKind: KINDS.OBJECT, typeName: this.subscriptionTypeName, fieldName: name, cleanup }) 511 | } 512 | 513 | // Removes all the undefined gaps created by various removals 514 | cleanSchema() { 515 | // Used to compare the schema before and after it was cleaned 516 | const schemaToStart = JSON.stringify(this.schema) 517 | const typesEncountered = new Set() 518 | const types = [] 519 | 520 | const interfacesEncounteredKeys = new Set() 521 | const interfacesByKey = {} 522 | 523 | // The Query, Mutation and Subscription Types should never be removed due to not being referenced 524 | // by anything 525 | if (this.queryTypeName) { 526 | typesEncountered.add(buildKey({ kind: KINDS.OBJECT, name: this.queryTypeName })) 527 | } 528 | if (this.mutationTypeName) { 529 | typesEncountered.add(buildKey({ kind: KINDS.OBJECT, name: this.mutationTypeName })) 530 | } 531 | if (this.subscriptionTypeName) { 532 | typesEncountered.add(buildKey({ kind: KINDS.OBJECT, name: this.subscriptionTypeName })) 533 | } 534 | 535 | for (const directive of this.schema.directives) { 536 | if (!directive) { 537 | continue 538 | } 539 | const args = [] 540 | for (const arg of directive.args) { 541 | const argType = digUnderlyingType(arg.type) 542 | // Don't add it if its return type does not exist 543 | if (!this._hasType(argType)) { 544 | continue 545 | } 546 | 547 | // Keep track of this so we know what we can remove 548 | typesEncountered.add(buildKey(argType)) 549 | 550 | args.push(arg) 551 | } 552 | 553 | directive.args = args 554 | } 555 | 556 | for (const type of this.schema.types) { 557 | if (!type) { 558 | continue 559 | } 560 | 561 | types.push(type) 562 | 563 | if (type.kind === KINDS.INTERFACE) { 564 | interfacesByKey[buildKey(type)] = type 565 | } 566 | 567 | if (type.fields) { 568 | const fields = [] 569 | for (const field of type.fields) { 570 | if (isUndef(field)) { 571 | continue 572 | } 573 | 574 | const fieldType = digUnderlyingType(field.type) 575 | 576 | // Don't add it if its return type does not exist 577 | if (!this._hasType(fieldType)) { 578 | continue 579 | } 580 | 581 | if (fieldType.kind === KINDS.INTERFACE) { 582 | interfacesEncounteredKeys.add(buildKey(fieldType)) 583 | } 584 | 585 | // Keep track of this so we know what we can remove 586 | typesEncountered.add(buildKey(fieldType)) 587 | 588 | const args = [] 589 | for (const arg of (field.args || [])) { 590 | if (isUndef(arg)) { 591 | continue 592 | } 593 | 594 | const argType = digUnderlyingType(arg.type) 595 | // Don't add it if its return type does not exist 596 | if (!this._hasType(argType)) { 597 | continue 598 | } 599 | 600 | // Keep track of this so we know what we can remove 601 | typesEncountered.add(buildKey(argType)) 602 | 603 | args.push(arg) 604 | } 605 | 606 | // Args will always be an array. Possible empty, but never null 607 | field.args = args 608 | fields.push(field) 609 | } 610 | 611 | type.fields = fields 612 | } 613 | 614 | if (type.inputFields) { 615 | const inputFields = [] 616 | // Don't add it in if it's undefined, or the type is gone 617 | for (const inputField of type.inputFields) { 618 | if (isUndef(inputField)) { 619 | continue 620 | } 621 | 622 | const inputFieldType = digUnderlyingType(inputField.type) 623 | // Don't add it if its return type does not exist 624 | if (!this._hasType(inputFieldType)) { 625 | continue 626 | } 627 | 628 | // Keep track of this so we know what we can remove 629 | typesEncountered.add(buildKey(inputFieldType)) 630 | 631 | inputFields.push(inputField) 632 | } 633 | 634 | type.inputFields = inputFields 635 | } 636 | 637 | if (type.possibleTypes) { 638 | const possibleTypes = [] 639 | for (const possibleType of type.possibleTypes) { 640 | if (isUndef(possibleType)) { 641 | continue 642 | } 643 | 644 | // possibleTypes array entries have no envelope for the type 645 | // so do not do possibleType.type here 646 | const possibleTypeType = digUnderlyingType(possibleType) 647 | // Don't add it if its return type does not exist 648 | if (!this._hasType(possibleTypeType)) { 649 | continue 650 | } 651 | 652 | // Interfaces themselves list all the things that have "implemented" them 653 | // in the "possibleTypes" array...but we don't want that to be an indication 654 | // that the thing has been used. 655 | if (type.kind !== KINDS.INTERFACE) { 656 | // Keep track of this so we know what we can remove 657 | typesEncountered.add(buildKey(possibleTypeType)) 658 | } 659 | 660 | possibleTypes.push(possibleType) 661 | } 662 | 663 | type.possibleTypes = possibleTypes 664 | } 665 | 666 | if (type.interfaces) { 667 | const interfaces = [] 668 | for (const interfayce of type.interfaces) { 669 | if (isUndef(interfayce)) { 670 | continue 671 | } 672 | 673 | if (!this._hasType(interfayce)) { 674 | continue 675 | } 676 | 677 | typesEncountered.add(buildKey(interfayce)) 678 | 679 | interfaces.push(interfayce) 680 | } 681 | 682 | type.interfaces = interfaces 683 | } 684 | } 685 | 686 | for (const interfaceEncounteredKey of interfacesEncounteredKeys.values()) { 687 | const interfayce = interfacesByKey[interfaceEncounteredKey] 688 | for (const possibleType of interfayce?.possibleTypes || []) { 689 | if (!this._hasType(possibleType)) { 690 | continue 691 | } 692 | 693 | // Keep track of this so we know what we can remove 694 | typesEncountered.add(buildKey(possibleType)) 695 | } 696 | } 697 | 698 | // Only include Types that we encountered - if the options say to do so 699 | const possiblyFilteredTypes = this.opts.removeUnusedTypes ? types.filter((type) => isReservedType(type) || typesEncountered.has(buildKey(type))) : types 700 | 701 | // Replace the Schema 702 | this.schema = { 703 | ...this.schema, 704 | types: possiblyFilteredTypes, 705 | } 706 | 707 | // Need to re-analyze it, too 708 | this._analyze() 709 | 710 | // If the schema was changed by this cleanup, we should run it again to see if other things 711 | // should be removed...and continue to do so until the schema is stable. 712 | if (schemaToStart !== JSON.stringify(this.schema)) { 713 | return this.cleanSchema() 714 | } 715 | } 716 | 717 | //****************************************************************** 718 | // 719 | // 720 | // PRIVATE 721 | // 722 | // 723 | 724 | _validate() { 725 | if (!this.schema) { 726 | throw new Error('No schema property detected!') 727 | } 728 | 729 | if (!this.schema.types) { 730 | throw new Error('No types detected!') 731 | } 732 | 733 | // Must have a Query type...but not necessarily a Mutation type 734 | if (!get(this.schema, `queryType.name`)) { 735 | throw new Error(`No queryType detected!`) 736 | } 737 | } 738 | 739 | _analyze() { 740 | // Map the kind + name to the index in the types array 741 | this.typeToIndexMap = {} 742 | this.fieldsOfTypeMap = {} 743 | this.inputFieldsOfTypeMap = {} 744 | // AKA Unions 745 | this.possibleTypesOfTypeMap = {} 746 | this.argsOfTypeMap = {} 747 | 748 | this.directiveToIndexMap = {} 749 | this.directiveArgsOfTypeMap = {} 750 | 751 | // Need to keep track of these so that we never remove them for not being referenced 752 | this.queryTypeName = get(this.schema, 'queryType.name') 753 | this.mutationTypeName = get(this.schema, 'mutationType.name') 754 | this.subscriptionTypeName = get(this.schema, 'subscriptionType.name') 755 | 756 | for (let directivesIdx = 0; directivesIdx < this.schema.directives.length; directivesIdx++) { 757 | const directive = this.schema.directives[directivesIdx] 758 | if (isUndef(directive)) { 759 | continue 760 | } 761 | 762 | const directivesKey = buildKey({ kind: 'DIRECTIVE', name: directive.name }) 763 | this.directiveToIndexMap[directivesKey] = directivesIdx 764 | 765 | const directivePath = `directives.${directivesIdx}` 766 | 767 | for (let argsIdx = 0; argsIdx < directive.args.length; argsIdx++) { 768 | const arg = directive.args[argsIdx] 769 | if (isUndef(arg)) { 770 | continue 771 | } 772 | const argType = digUnderlyingType(arg.type) 773 | const argsKey = buildKey(argType) 774 | if (!this.directiveArgsOfTypeMap[argsKey]) { 775 | this.directiveArgsOfTypeMap[argsKey] = [] 776 | } 777 | 778 | const argPath = `${directivePath}.args.${argsIdx}` 779 | this.directiveArgsOfTypeMap[argsKey].push(argPath) 780 | } 781 | } 782 | 783 | for (let typesIdx = 0; typesIdx < this.schema.types.length; typesIdx++) { 784 | const type = this.schema.types[typesIdx] 785 | if (isUndef(type)) { 786 | continue 787 | } 788 | 789 | const { 790 | kind, 791 | name, 792 | } = type 793 | // These come in as null, not undefined 794 | const fields = type.fields || [] 795 | const inputFields = type.inputFields || [] 796 | const possibleTypes = type.possibleTypes || [] 797 | 798 | const typesKey = buildKey({ kind, name }) 799 | this.typeToIndexMap[typesKey] = typesIdx 800 | 801 | for (let fieldsIdx = 0; fieldsIdx < fields.length; fieldsIdx++) { 802 | const field = fields[fieldsIdx] 803 | if (isUndef(field)) { 804 | continue 805 | } 806 | 807 | const fieldType = digUnderlyingType(field.type) 808 | // This should always be arrays...maybe empty, never null 809 | const args = field.args || [] 810 | 811 | const fieldsKey = buildKey(fieldType) 812 | if (!this.fieldsOfTypeMap[fieldsKey]) { 813 | this.fieldsOfTypeMap[fieldsKey] = [] 814 | } 815 | 816 | const fieldPath = `types.${typesIdx}.fields.${fieldsIdx}` 817 | this.fieldsOfTypeMap[fieldsKey].push(fieldPath) 818 | 819 | for (let argsIdx = 0; argsIdx < args.length; argsIdx++) { 820 | const arg = args[argsIdx] 821 | if (isUndef(arg)) { 822 | continue 823 | } 824 | const argType = digUnderlyingType(arg.type) 825 | 826 | const argsKey = buildKey(argType) 827 | if (!this.argsOfTypeMap[argsKey]) { 828 | this.argsOfTypeMap[argsKey] = [] 829 | } 830 | 831 | const argPath = `${fieldPath}.args.${argsIdx}` 832 | this.argsOfTypeMap[argsKey].push(argPath) 833 | } 834 | } 835 | 836 | for (let inputFieldsIdx = 0; inputFieldsIdx < inputFields.length; inputFieldsIdx++) { 837 | const inputField = inputFields[inputFieldsIdx] 838 | if (isUndef(inputField)) { 839 | continue 840 | } 841 | const inputFieldType = digUnderlyingType(inputField.type) 842 | const inputFieldsKey = buildKey(inputFieldType) 843 | if (!this.inputFieldsOfTypeMap[inputFieldsKey]) { 844 | this.inputFieldsOfTypeMap[inputFieldsKey] = [] 845 | } 846 | const inputFieldPath = `types.${typesIdx}.inputFields.${inputFieldsIdx}` 847 | this.inputFieldsOfTypeMap[inputFieldsKey].push(inputFieldPath) 848 | } 849 | 850 | for (let possibleTypesIdx = 0; possibleTypesIdx < possibleTypes.length; possibleTypesIdx++) { 851 | const possibleType = possibleTypes[possibleTypesIdx] 852 | if (isUndef(possibleType)) { 853 | continue 854 | } 855 | 856 | const possibleTypeType = digUnderlyingType(possibleType) 857 | const possibleTypeKey = buildKey(possibleTypeType) 858 | if (!this.possibleTypesOfTypeMap[possibleTypeKey]) { 859 | this.possibleTypesOfTypeMap[possibleTypeKey] = [] 860 | } 861 | const possibleTypePath = `types.${typesIdx}.possibleTypes.${possibleTypesIdx}` 862 | this.possibleTypesOfTypeMap[possibleTypeKey].push(possibleTypePath) 863 | } 864 | } 865 | } 866 | 867 | _fixQueryAndMutationAndSubscriptionTypes(response) { 868 | for (const [key, defaultTypeName] of [['queryType', 'Query'], ['mutationType', 'Mutation'], ['subscriptionType', 'Subscription']]) { 869 | const queryOrMutationOrSubscriptionTypeName = get(response, `__schema.${key}.name`) 870 | if (queryOrMutationOrSubscriptionTypeName && !this.getType({ kind: KINDS.OBJECT, name: queryOrMutationOrSubscriptionTypeName })) { 871 | this.schema[key] = { name: defaultTypeName } 872 | } 873 | } 874 | } 875 | 876 | _getTypeIndex({ kind, name }) { 877 | const key = buildKey({ kind, name }) 878 | if (Object.prototype.hasOwnProperty.call(this.typeToIndexMap, key)) { 879 | return this.typeToIndexMap[key] 880 | } 881 | 882 | return false 883 | } 884 | 885 | _getDirectiveIndex({ name }) { 886 | const key = buildKey({ kind: 'DIRECTIVE', name }) 887 | if (Object.prototype.hasOwnProperty.call(this.directiveToIndexMap, key)) { 888 | return this.directiveToIndexMap[key] 889 | } 890 | 891 | return false 892 | } 893 | 894 | _removeThingsOfType({ kind, name, map, cleanup = true }) { 895 | const key = buildKey({ kind, name }) 896 | for (const path of (map[key] || [])) { 897 | unset(this.schema, path) 898 | } 899 | 900 | delete map[key] 901 | 902 | if (cleanup) { 903 | this.cleanSchema() 904 | } 905 | } 906 | 907 | _removeFieldsOfType({ 908 | kind, 909 | name, 910 | // Clean up the schema afterwards? 911 | cleanup = true, 912 | }) { 913 | return this._removeThingsOfType({ kind, name, map: this.fieldsOfTypeMap, cleanup }) 914 | } 915 | 916 | _removeInputFieldsOfType({ 917 | kind, 918 | name, 919 | // Clean up the schema afterwards? 920 | cleanup = true, 921 | }) { 922 | return this._removeThingsOfType({ kind, name, map: this.inputFieldsOfTypeMap, cleanup }) 923 | } 924 | 925 | // AKA Unions 926 | _removePossibleTypesOfType({ 927 | kind, 928 | name, 929 | // Clean up the schema afterwards? 930 | cleanup = true, 931 | }) { 932 | return this._removeThingsOfType({ kind, name, map: this.possibleTypesOfTypeMap, cleanup }) 933 | } 934 | 935 | _removeArgumentsOfType({ 936 | kind, 937 | name, 938 | // Clean up the schema afterwards? 939 | cleanup = true, 940 | }) { 941 | return this._removeThingsOfType({ kind, name, map: this.argsOfTypeMap, cleanup }) 942 | } 943 | 944 | _removeDirectiveArgumentsOfType({ 945 | // kind, 946 | name, 947 | // Clean up the schema afterwards? 948 | cleanup = true, 949 | }) { 950 | return this._removeThingsOfType({ kind: 'DIRECTIVE', name, map: this.directiveArgsOfTypeMap, cleanup }) 951 | } 952 | 953 | _cloneSchema() { 954 | return JSON.parse(JSON.stringify(this.schema)) 955 | } 956 | 957 | _hasType({ kind, name }) { 958 | const key = buildKey({ kind, name }) 959 | return Object.prototype.hasOwnProperty.call(this.typeToIndexMap, key) 960 | } 961 | } 962 | 963 | // A function that digs through any Non-Null and List nesting and returns the underlying Type. 964 | export function digUnderlyingType(type) { 965 | while ([KINDS.NON_NULL, KINDS.LIST].includes(type.kind)) { 966 | type = type.ofType 967 | } 968 | return type 969 | } 970 | 971 | // A function that returns a Boolean indicating whether a Type is special GraphQL reserved Type. 972 | export function isReservedType(type) { 973 | return type.name.startsWith('__') 974 | } 975 | 976 | function buildKey({ kind, name }) { 977 | return kind + ':' + name 978 | } 979 | 980 | function isUndef(item) { 981 | return typeof item === 'undefined' 982 | } 983 | 984 | function mapProps({ props, map }) { 985 | return Object.entries(map).reduce( 986 | (acc, [from, to]) => { 987 | if (Object.prototype.hasOwnProperty.call(props, from)) { 988 | acc[to] = props[from] 989 | } 990 | return acc 991 | }, 992 | {}, 993 | ) 994 | } 995 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash.isequal' 2 | import { 3 | Microfiber, 4 | KINDS, 5 | } from '../index' 6 | 7 | import { 8 | introspectionResponseFromSchemaSDL, 9 | } from './test-helpers' 10 | 11 | 12 | describe('index', function () { 13 | def('QueryType', () => `type Query { 14 | myTypes: [MyType!] 15 | myMyOtherInterfaceTypes: [MyOtherInterface] 16 | myOtherOtherInterfaceType: MyOtherOtherInterfaceType 17 | myUnions: [MyUnion] 18 | myOtherUnionType: MyOtherUnionType 19 | }` 20 | ) 21 | 22 | def('MutationType', () => `type Mutation { 23 | createYetAnotherType(name: String!): YetAnotherType! 24 | }` 25 | ) 26 | 27 | def('SubscriptionType', () => `type Subscription { 28 | subscribeToMyTypeFieldStringChanges(myTypeId: ID): RandomTypeOne! 29 | }` 30 | ) 31 | 32 | def('schemaSDLBase', () => ` 33 | 34 | # From the GraphQL docs: 35 | # 36 | # https://graphql.org/graphql-js/mutations-and-input-types/ 37 | # Input types can't have fields that are other objects, only basic scalar types, list types, 38 | # and other input types. 39 | 40 | scalar SecretScalar 41 | 42 | enum SecretEnum { 43 | ENUM1 44 | ENUM2 45 | ENUM3 46 | } 47 | 48 | input InputWithSecretScalar { 49 | string: String 50 | secretScalar: [SecretScalar] 51 | } 52 | 53 | input InputWithSecretEnum { 54 | string: String 55 | otherString: String 56 | secretEnum: [SecretEnum] 57 | } 58 | 59 | union SecretUnion = 60 | MyType 61 | | MyOtherType 62 | 63 | ${$.QueryType} 64 | 65 | ${$.MutationType} 66 | 67 | ${$.SubscriptionType} 68 | 69 | interface MyInterface { 70 | id: String! 71 | } 72 | 73 | type MyType implements MyInterface { 74 | # required due to Interface 75 | id: String! 76 | 77 | # The control 78 | fieldString(argString: String): String 79 | 80 | # SecretScalar stuff 81 | 82 | # Fields returning SecretScalar 83 | fieldSecretScalar: SecretScalar 84 | fieldSecretScalarArray: [SecretScalar] 85 | fieldSecretScalarNonNullArray: [SecretScalar]! 86 | fieldSecretScalarNonNullArrayOfNonNulls: [SecretScalar!]! 87 | 88 | # Fields with args containing SecretScalars 89 | fieldStringWithSecretScalarArg( 90 | argString: String, 91 | argSecretScalar: SecretScalar 92 | ): String 93 | 94 | fieldStringWithSecretScalarArrayArg( 95 | argString: String, 96 | argSecretScalar: [SecretScalar] 97 | ): String 98 | 99 | fieldStringWithSecretScalarNonNullArrayArg( 100 | argString: String, 101 | argSecretScalar: [SecretScalar]! 102 | ): String 103 | 104 | fieldStringWithSecretScalarNonNullArrayOfNonNullsArg( 105 | argString: String, 106 | argSecretScalar: [SecretScalar!]! 107 | ): String 108 | 109 | # Fields with Inputs that contain SecretScalar 110 | fieldWithSecretScalarInputArg(input: InputWithSecretScalar): String 111 | 112 | # Fields with Inputs that contain SecretEnum 113 | fieldWithSecretEnumInputArg(input: InputWithSecretEnum): String 114 | 115 | 116 | # SecretEnum stuff 117 | 118 | # Fields returning SecretEnum 119 | fieldSecretEnum: SecretEnum 120 | fieldSecretEnumArray: [SecretEnum] 121 | fieldSecretEnumNonNullArray: [SecretEnum]! 122 | fieldSecretEnumNonNullArrayOfNonNulls: [SecretEnum!]! 123 | 124 | # Fields with args containing SecretEnum 125 | fieldStringWithSecretEnumArg( 126 | argString: String, 127 | argSecretEnum: SecretEnum 128 | ): String 129 | 130 | fieldStringWithSecretEnumArrayArg( 131 | argString: String, 132 | argSecretEnum: [SecretEnum] 133 | ): String 134 | 135 | fieldStringWithSecretEnumNonNullArrayArg( 136 | argString: String, 137 | argSecretEnum: [SecretEnum]! 138 | ): String 139 | 140 | fieldStringWithSecretEnumNonNullArrayOfNonNullsArg( 141 | argString: String, 142 | argSecretEnum: [SecretEnum!]! 143 | ): String 144 | 145 | 146 | # SecretUnion stuff 147 | 148 | # Fields returning SecretUnion 149 | fieldSecretUnion: SecretUnion 150 | fieldSecretUnionArray: [SecretUnion] 151 | fieldSecretUnionNonNullArray: [SecretUnion]! 152 | fieldSecretUnionNonNullArrayOfNonNulls: [SecretUnion!]! 153 | } 154 | 155 | type MyOtherType { 156 | fieldString(argString: String): String 157 | } 158 | 159 | type YetAnotherType { 160 | fieldString: String 161 | } 162 | 163 | type RandomTypeOne { 164 | fieldString: String 165 | } 166 | 167 | "Should not show up because it was not used anywhere" 168 | type NotUsed { 169 | referencedButNotUsed: ReferencedButNotUsed 170 | } 171 | 172 | "Should not show up because the only thing that references it was not used" 173 | type ReferencedButNotUsed { 174 | name: String 175 | } 176 | 177 | "I am definitely not used at all" 178 | type TotallyNotUsed { 179 | name: String 180 | } 181 | 182 | input DirectiveOption { 183 | key: String! 184 | value: String! 185 | } 186 | 187 | input AnotherDirectiveOption { 188 | key: String! 189 | value: String! 190 | } 191 | 192 | interface MyOtherInterface { 193 | id: String! 194 | } 195 | 196 | type MyOtherInterfaceType implements MyOtherInterface { 197 | id: String! 198 | foo: String 199 | } 200 | 201 | type MyOtherOtherInterfaceType implements MyOtherInterface { 202 | id: String! 203 | foo: String 204 | } 205 | 206 | type MyUnionType { 207 | foo: String 208 | } 209 | 210 | type MyOtherUnionType { 211 | bar: String 212 | } 213 | 214 | union MyUnion = MyUnionType | MyOtherUnionType 215 | 216 | directive @foo(option: DirectiveOption, anotherOption: AnotherDirectiveOption) on OBJECT 217 | ` 218 | ) 219 | def('schemaSDL', () => $.schemaSDLBase) 220 | 221 | def('metadataBase', () => ({ 222 | 'OBJECT': { 223 | MyType: { 224 | 225 | }, 226 | OtherType: { 227 | 228 | }, 229 | Query: { 230 | 231 | }, 232 | Mutation: { 233 | 234 | } 235 | }, 236 | 'INPUT_OBJECT': { 237 | MyInput: { 238 | 239 | }, 240 | } 241 | })) 242 | 243 | def('metadata', () => $.metadataBase) 244 | 245 | def('rawResponse', () => introspectionResponseFromSchemaSDL({ 246 | schemaSDL: $.schemaSDL 247 | })) 248 | 249 | def('response', () => $.rawResponse) 250 | 251 | def('schema', () => $.response.__schema) 252 | 253 | it('works', function () { 254 | let microfiber = new Microfiber($.response, { cleanupSchemaImmediately: false }) 255 | let response = microfiber.getResponse() 256 | 257 | //************************ 258 | // 259 | // Sanity checks 260 | expect(isEqual($.response, response)).to.be.true 261 | 262 | microfiber = new Microfiber($.response) 263 | response = microfiber.getResponse() 264 | // Some cleanup occurred right away 265 | expect(isEqual($.response, response)).to.not.be.true 266 | 267 | let queryType = microfiber.getQueryType() 268 | expect(queryType).to.be.ok 269 | expect(queryType).to.eql(findType({ kind: KINDS.OBJECT, name: 'Query', response })) 270 | expect(microfiber.getQuery({ name: 'myTypes' })).be.ok 271 | 272 | let mutationType = microfiber.getMutationType() 273 | expect(mutationType).to.be.ok 274 | expect(mutationType).to.eql(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })) 275 | expect(microfiber.getMutation({ name: 'createYetAnotherType' })).be.ok 276 | 277 | let subscriptionType = microfiber.getSubscriptionType() 278 | expect(subscriptionType).to.be.ok 279 | expect(subscriptionType).to.eql(findType({ kind: KINDS.OBJECT, name: 'Subscription', response })) 280 | expect(microfiber.getSubscription({ name: 'subscribeToMyTypeFieldStringChanges' })).be.ok 281 | 282 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyInterface' })).to.be.ok 283 | let myInterfaceId = microfiber.getField({ typeKind: KINDS.INTERFACE, typeName: 'MyInterface', fieldName: 'id' }) 284 | expect(myInterfaceId).to.be.ok 285 | expect(microfiber.getInterfaceField({ typeName: 'MyInterface', fieldName: 'id' })).to.eql(myInterfaceId) 286 | 287 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyOtherInterface' })).to.be.ok 288 | expect(microfiber.getType({ name: 'MyOtherInterfaceType' })).to.be.ok 289 | expect(microfiber.getType({ name: 'MyOtherOtherInterfaceType' })).to.be.ok 290 | 291 | expect(microfiber.getType({ kind: KINDS.UNION, name: 'MyUnion' })).to.be.ok 292 | expect(microfiber.getType({ name: 'MyUnionType' })).to.be.ok 293 | expect(microfiber.getType({ name: 'MyOtherUnionType' })).to.be.ok 294 | 295 | expect(microfiber.getDirective({ name: 'foo' })).to.be.ok 296 | expect(microfiber.getDirectiveArg({ directiveName: 'foo', argName: 'option' })).to.be.ok 297 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'DirectiveOption' })).to.be.ok 298 | expect(microfiber.getDirectiveArg({ directiveName: 'foo', argName: 'anotherOption' })).to.be.ok 299 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'AnotherDirectiveOption' })).to.be.ok 300 | 301 | expect(microfiber.getType({ name: 'NotUsed' })).to.not.be.ok 302 | expect(microfiber.getType({ name: 'ReferencedButNotUsed' })).to.not.be.ok 303 | expect(microfiber.getType({ name: 'TotallyNotUsed' })).to.not.be.ok 304 | 305 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', response })).to.be.ok 306 | expect(findType({ kind: KINDS.SCALAR, name: 'SecretScalar', response })).to.be.ok 307 | 308 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalar', response })).to.be.ok 309 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalarArray', response })).to.be.ok 310 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalarNonNullArray', response })).to.be.ok 311 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalarNonNullArrayOfNonNulls', response })).to.be.ok 312 | 313 | let arg = microfiber.getArg({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarArg', argName: 'argSecretScalar', response }) 314 | expect(arg).to.be.ok 315 | expect(arg).to.eql(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarArg', argName: 'argSecretScalar', response })) 316 | arg = microfiber.getArg({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarArg', argName: 'argSecretScalar', response }) 317 | expect(arg).to.be.ok 318 | arg = microfiber.getArg({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarArrayArg', argName: 'argSecretScalar', response }) 319 | expect(arg).to.be.ok 320 | arg = microfiber.getArg({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarNonNullArrayArg', argName: 'argSecretScalar', response }) 321 | expect(arg).to.be.ok 322 | arg = microfiber.getArg({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarNonNullArrayOfNonNullsArg', argName: 'argSecretScalar', response }) 323 | expect(arg).to.be.ok 324 | 325 | // Won't work because typeKind defaults to OBJECT 326 | expect(microfiber.getField({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.not.be.ok 327 | // This works, though 328 | expect(microfiber.getField({ typeKind: KINDS.INPUT_OBJECT, typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.be.ok 329 | expect(microfiber.getInputField({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.be.ok 330 | expect(microfiber.getInputField({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.eql( 331 | microfiber.getField({ typeKind: KINDS.INPUT_OBJECT, typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' }) 332 | ) 333 | expect(microfiber.getInputField({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.eql( 334 | findInputFieldOnInputType({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar', response }) 335 | ) 336 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretScalarInputArg' })).to.be.ok 337 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretScalarInputArg' })).to.eql( 338 | findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretScalarInputArg', response }) 339 | ) 340 | 341 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum' })).to.be.ok 342 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum' })).to.eql( 343 | findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum', response }) 344 | ) 345 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg', response })).to.be.ok 346 | 347 | // 348 | // 349 | //************************ 350 | 351 | let secretEnum = findType({ kind: KINDS.ENUM, name: 'SecretEnum', response }) 352 | expect(secretEnum).to.be.ok 353 | expect(secretEnum.enumValues).to.be.an('array').of.length(3) 354 | expect(secretEnum.enumValues.map((enumValue) => enumValue.name)).eql(['ENUM1', 'ENUM2', 'ENUM3']) 355 | expect(secretEnum).to.eql(microfiber.getType({ kind: KINDS.ENUM, name: 'SecretEnum' })) 356 | 357 | let secretEnumValue = microfiber.getEnumValue({ typeName: 'SecretEnum', fieldName: 'ENUM2' }) 358 | expect(secretEnumValue).to.be.ok 359 | expect(secretEnumValue.name).to.eql('ENUM2') 360 | 361 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnum', response })).to.be.ok 362 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumArray', response })).to.be.ok 363 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumNonNullArray', response })).to.be.ok 364 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumNonNullArrayOfNonNulls', response })).to.be.ok 365 | 366 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumArg', argName: 'argSecretEnum', response })).to.be.ok 367 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumArrayArg', argName: 'argSecretEnum', response })).to.be.ok 368 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumNonNullArrayArg', argName: 'argSecretEnum', response })).to.be.ok 369 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumNonNullArrayOfNonNullsArg', argName: 'argSecretEnum', response })).to.be.ok 370 | 371 | 372 | expect(findType({ kind: KINDS.UNION, name: 'SecretUnion', response})).to.be.ok 373 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnion', response })).to.be.ok 374 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionArray', response })).to.be.ok 375 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArray', response })).to.be.ok 376 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArrayOfNonNulls', response })).to.be.ok 377 | 378 | // OK, let's do some things 379 | 380 | // Remove SecretScalar 381 | microfiber.removeType({ kind: KINDS.SCALAR, name: 'SecretScalar' }) 382 | response = microfiber.getResponse() 383 | expect(isEqual($.response, response)).to.be.false 384 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 385 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 386 | expect(findType({ kind: KINDS.OBJECT, name: 'Subscription', response })).to.be.ok 387 | 388 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', response })).to.be.ok 389 | expect(findType({ kind: KINDS.SCALAR, name: 'SecretScalar', response })).to.not.be.ok 390 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalar', response })).to.not.be.ok 391 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalarArray', response })).to.not.be.ok 392 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalarNonNullArray', response })).to.not.be.ok 393 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretScalarNonNullArrayOfNonNulls', response })).to.not.be.ok 394 | 395 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarArg', argName: 'argSecretScalar', response })).to.not.be.ok 396 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarArrayArg', argName: 'argSecretScalar', response })).to.not.be.ok 397 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarNonNullArrayArg', argName: 'argSecretScalar', response })).to.not.be.ok 398 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretScalarNonNullArrayOfNonNullsArg', argName: 'argSecretScalar', response })).to.not.be.ok 399 | 400 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar', response })).to.not.be.ok 401 | expect(microfiber.getField({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.not.be.ok 402 | expect(microfiber.getField({ typeKind: KINDS.INPUT_OBJECT, typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.not.be.ok 403 | expect(microfiber.getInputField({ typeName: 'InputWithSecretScalar', fieldName: 'secretScalar' })).to.not.be.ok 404 | // these should still be ok 405 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretScalarInputArg' })).to.be.ok 406 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretScalarInputArg' })).to.eql( 407 | findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretScalarInputArg', response }) 408 | ) 409 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum', response })).to.be.ok 410 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg', response })).to.be.ok 411 | 412 | // Remove a specific SecretEnum value 413 | 414 | secretEnum = findType({ kind: KINDS.ENUM, name: 'SecretEnum', response }) 415 | expect(secretEnum).to.be.ok 416 | expect(secretEnum.enumValues).to.be.an('array').of.length(3) 417 | 418 | microfiber.removeEnumValue({ name: 'SecretEnum', value: 'ENUM2' }) 419 | response = microfiber.getResponse() 420 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 421 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 422 | expect(findType({ kind: KINDS.OBJECT, name: 'Subscription', response })).to.be.ok 423 | 424 | // Removed that value so only 2 left 425 | secretEnum = findType({ kind: KINDS.ENUM, name: 'SecretEnum', response }) 426 | expect(secretEnum).to.be.ok 427 | expect(secretEnum.enumValues).to.be.an('array').of.length(2) 428 | expect(secretEnum.enumValues.map((enumValue) => enumValue.name)).eql(['ENUM1', 'ENUM3']) 429 | 430 | secretEnumValue = microfiber.getEnumValue({ typeName: 'SecretEnum', fieldName: 'ENUM2' }) 431 | expect(secretEnumValue).to.not.be.ok 432 | 433 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnum', response })).to.be.ok 434 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumArray', response })).to.be.ok 435 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumNonNullArray', response })).to.be.ok 436 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumNonNullArrayOfNonNulls', response })).to.be.ok 437 | 438 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumArg', argName: 'argSecretEnum', response })).to.be.ok 439 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumArrayArg', argName: 'argSecretEnum', response })).to.be.ok 440 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumNonNullArrayArg', argName: 'argSecretEnum', response })).to.be.ok 441 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumNonNullArrayOfNonNullsArg', argName: 'argSecretEnum', response })).to.be.ok 442 | 443 | 444 | // Remove SecretEnum completely 445 | 446 | microfiber.removeType({ kind: KINDS.ENUM, name: 'SecretEnum' }) 447 | response = microfiber.getResponse() 448 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 449 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 450 | expect(findType({ kind: KINDS.OBJECT, name: 'Subscription', response })).to.be.ok 451 | 452 | expect(findType({ kind: KINDS.ENUM, name: 'SecretEnum', response })).to.not.be.ok 453 | expect(microfiber.getType({ kind: KINDS.ENUM, name: 'SecretEnum' })).to.not.be.ok 454 | secretEnumValue = microfiber.getEnumValue({ typeName: 'SecretEnum', fieldName: 'ENUM1' }) 455 | expect(secretEnumValue).to.not.be.ok 456 | 457 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnum', response })).to.not.be.ok 458 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumArray', response })).to.not.be.ok 459 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumNonNullArray', response })).to.not.be.ok 460 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretEnumNonNullArrayOfNonNulls', response })).to.not.be.ok 461 | 462 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumArg', argName: 'argSecretEnum', response })).to.not.be.ok 463 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumArrayArg', argName: 'argSecretEnum', response })).to.not.be.ok 464 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumNonNullArrayArg', argName: 'argSecretEnum', response })).to.not.be.ok 465 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldStringWithSecretEnumNonNullArrayOfNonNullsArg', argName: 'argSecretEnum', response })).to.not.be.ok 466 | 467 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg' })).to.be.ok 468 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum', response })).to.not.be.ok 469 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg', response })).to.be.ok 470 | 471 | // Remove an Arg from a Field 472 | 473 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', argName: 'argString', response })).to.be.ok 474 | microfiber.removeArg({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', argName: 'argString' }) 475 | response = microfiber.getResponse() 476 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 477 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 478 | expect(findType({ kind: KINDS.OBJECT, name: 'Subscription', response })).to.be.ok 479 | expect(findArgOnFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', argName: 'argString', response })).to.not.be.ok 480 | 481 | // Remove a Field from a Type 482 | 483 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', response })).to.be.ok 484 | microfiber.removeField({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString' }) 485 | response = microfiber.getResponse() 486 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 487 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 488 | expect(findType({ kind: KINDS.OBJECT, name: 'Subscription', response })).to.be.ok 489 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldString', response })).to.not.be.ok 490 | 491 | // Remove an Input Field from an Input Object 492 | 493 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg' })).to.be.ok 494 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg' })).to.eql( 495 | findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg', response }) 496 | ) 497 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum' })).to.not.be.ok 498 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'secretEnum', response })).to.not.be.ok 499 | 500 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'string' })).to.be.ok 501 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'string', response })).to.be.ok 502 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'otherString' })).to.be.ok 503 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'otherString', response })).to.be.ok 504 | 505 | // This won't work due to typeKind defaulting to OBJECT 506 | microfiber.removeField({ typeName: 'InputWithSecretEnum', fieldName: 'string' }) 507 | response = microfiber.getResponse() 508 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'string' })).to.be.ok 509 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'string', response })).to.be.ok 510 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'otherString' })).to.be.ok 511 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'otherString', response })).to.be.ok 512 | 513 | // But this will work... 514 | microfiber.removeField({ typeKind: KINDS.INPUT_OBJECT, typeName: 'InputWithSecretEnum', fieldName: 'string' }) 515 | response = microfiber.getResponse() 516 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'string' })).to.not.be.ok 517 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'string', response })).to.not.be.ok 518 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'otherString' })).to.be.ok 519 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'otherString', response })).to.be.ok 520 | 521 | microfiber.removeInputField({ typeName: 'InputWithSecretEnum', fieldName: 'otherString' }) 522 | response = microfiber.getResponse() 523 | expect(microfiber.getInputField({ typeName: 'InputWithSecretEnum', fieldName: 'otherString' })).to.not.be.ok 524 | expect(findInputFieldOnInputType({ typeName: 'InputWithSecretEnum', fieldName: 'otherString', response })).to.not.be.ok 525 | // That's the last of the inputFields...so it should be empty and not null 526 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'InputWithSecretEnum' })).to.be.ok 527 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'InputWithSecretEnum' }).inputFields).to.eql([]) 528 | 529 | 530 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg' })).to.be.ok 531 | expect(microfiber.getField({ typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg' })).to.eql( 532 | findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldWithSecretEnumInputArg', response }) 533 | ) 534 | 535 | // Remove in Input Type completely 536 | 537 | // Remove possible type from a Union Type 538 | 539 | let unionType = findType({ kind: KINDS.UNION, name: 'SecretUnion', response }) 540 | expect(unionType).to.be.ok 541 | expect(unionType.possibleTypes).to.be.an('array').of.length(2) 542 | 543 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnion', response })).to.be.ok 544 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionArray', response })).to.be.ok 545 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArray', response })).to.be.ok 546 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArrayOfNonNulls', response })).to.be.ok 547 | 548 | microfiber.removePossibleType({ typeName: 'SecretUnion', possibleTypeKind: KINDS.OBJECT, possibleTypeName: 'MyType' }) 549 | response = microfiber.getResponse() 550 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 551 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 552 | 553 | unionType = microfiber.getType({ kind: KINDS.UNION, name: 'SecretUnion' }) 554 | expect(unionType).to.be.ok 555 | expect(unionType).to.eql( 556 | findType({ kind: KINDS.UNION, name: 'SecretUnion', response }) 557 | ) 558 | expect(unionType.possibleTypes).to.be.an('array').of.length(1) 559 | // Only MyOtherType is left 560 | expect(unionType.possibleTypes.map((possibleType) => possibleType.name)).to.eql(['MyOtherType']) 561 | 562 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnion', response })).to.be.ok 563 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionArray', response })).to.be.ok 564 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArray', response })).to.be.ok 565 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArrayOfNonNulls', response })).to.be.ok 566 | 567 | // Remove a Union Type completely 568 | 569 | microfiber.removeType({ kind: KINDS.UNION, name: 'SecretUnion' }) 570 | response = microfiber.getResponse() 571 | expect(findType({ kind: KINDS.OBJECT, name: 'Query', response })).to.be.ok 572 | expect(findType({ kind: KINDS.OBJECT, name: 'Mutation', response })).to.be.ok 573 | 574 | expect(findType({ kind: KINDS.UNION, name: 'SecretUnion', response})).to.not.be.ok 575 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnion', response })).to.not.be.ok 576 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionArray', response })).to.not.be.ok 577 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArray', response })).to.not.be.ok 578 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'MyType', fieldName: 'fieldSecretUnionNonNullArrayOfNonNulls', response })).to.not.be.ok 579 | 580 | // Remove the myTypes Query...which should remove MyType as well now that there are no references to it 581 | 582 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'Query', fieldName: 'myTypes', response })).to.be.ok 583 | expect(findType({ kind: KINDS.OBJECT, name: 'MyType', response })).to.be.ok 584 | 585 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyInterface' })).to.be.ok 586 | myInterfaceId = microfiber.getField({ typeKind: KINDS.INTERFACE, typeName: 'MyInterface', fieldName: 'id' }) 587 | expect(myInterfaceId).to.be.ok 588 | expect(microfiber.getInterfaceField({ typeName: 'MyInterface', fieldName: 'id' })).to.eql(myInterfaceId) 589 | 590 | microfiber.removeQuery({ name: 'myTypes' }) 591 | response = microfiber.getResponse() 592 | 593 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'Query', fieldName: 'myTypes', response })).to.not.be.ok 594 | expect(findType({ kind: KINDS.OBJECT, name: 'MyType', response })).to.not.be.ok 595 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyInterface' })).to.not.be.ok 596 | 597 | // Remove YetAnotherType...which should remove the createYetAnotherType Mutation as well now that there are no 598 | // references to it 599 | 600 | expect(findType({ kind: KINDS.OBJECT, name: 'YetAnotherType', response })).to.be.ok 601 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'Mutation', fieldName: 'createYetAnotherType', response })).to.be.ok 602 | 603 | microfiber.removeType({ kind: KINDS.OBJECT, name: 'YetAnotherType' }) 604 | response = microfiber.getResponse() 605 | 606 | expect(findType({ kind: KINDS.OBJECT, name: 'YetAnotherType', response })).to.not.be.ok 607 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'Mutation', fieldName: 'createYetAnotherType', response })).to.not.be.ok 608 | 609 | //******************************** 610 | // Remove the "subscribeToMyTypeFieldStringChanges" Subscription, which should remove the "RandomTypeOne" now that nothing 611 | // references it 612 | 613 | // Make sure they're there to start with 614 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'Subscription', fieldName: 'subscribeToMyTypeFieldStringChanges', response })).to.be.ok 615 | expect(findType({ kind: KINDS.OBJECT, name: 'RandomTypeOne', response })).to.be.ok 616 | 617 | // Remove them, and make sure they're not there 618 | microfiber.removeSubscription({ name: 'subscribeToMyTypeFieldStringChanges' }) 619 | response = microfiber.getResponse() 620 | expect(findFieldOnType({ typeKind: KINDS.OBJECT, typeName: 'Subscription', fieldName: 'subscribeToMyTypeFieldStringChanges', response })).to.not.be.ok 621 | expect(findType({ kind: KINDS.OBJECT, name: 'RandomTypeOne', response })).to.not.be.ok 622 | 623 | // 624 | // 625 | //******************************** 626 | 627 | 628 | //******************************* 629 | // Remove the DirectiveOption Type, which should remove it from the directive Args 630 | // 631 | 632 | microfiber.removeType({ kind: KINDS.INPUT_OBJECT, name: 'DirectiveOption' }) 633 | expect(microfiber.getDirective({ name: 'foo' })).to.be.ok 634 | expect(microfiber.getDirectiveArg({ directiveName: 'foo', argName: 'option' })).to.not.be.ok 635 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'DirectiveOption' })).to.not.be.ok 636 | expect(microfiber.getDirectiveArg({ directiveName: 'foo', argName: 'anotherOption' })).to.be.ok 637 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'AnotherDirectiveOption' })).to.be.ok 638 | 639 | 640 | // And now remove the "foo" directive, and the AnotherDirectiveOption should be cleaned up and gone 641 | microfiber.removeDirective({ name: 'foo' }) 642 | expect(microfiber.getDirective({ name: 'foo' })).to.not.be.ok 643 | expect(microfiber.getDirectiveArg({ directiveName: 'foo', argName: 'option' })).to.not.be.ok 644 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'DirectiveOption' })).to.not.be.ok 645 | expect(microfiber.getDirectiveArg({ directiveName: 'foo', argName: 'anotherOption' })).to.not.be.ok 646 | expect(microfiber.getType({ kind: KINDS.INPUT_OBJECT, name: 'AnotherDirectiveOption' })).to.not.be.ok 647 | 648 | 649 | // 650 | // 651 | //******************************* 652 | 653 | //******************************* 654 | // 655 | // Stuff regarding INTERFACE usages 656 | // 657 | 658 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyOtherInterface' })).to.be.ok 659 | expect(microfiber.getType({ name: 'MyOtherInterfaceType' })).to.be.ok 660 | expect(microfiber.getType({ name: 'MyOtherOtherInterfaceType' })).to.be.ok 661 | 662 | // Remove the query that uses MyOtherOtherInterfaceType 663 | microfiber.removeQuery({ name: 'myOtherOtherInterfaceType' }) 664 | 665 | // Both are still here because myMyOtherInterfaceTypes query returns the Interface 666 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyOtherInterface' })).to.be.ok 667 | expect(microfiber.getType({ name: 'MyOtherInterfaceType' })).to.be.ok 668 | expect(microfiber.getType({ name: 'MyOtherOtherInterfaceType' })).to.be.ok 669 | 670 | // Remove the query that uses MyOtherInterface 671 | microfiber.removeQuery({ name: 'myMyOtherInterfaceTypes' }) 672 | 673 | // Everything is gone now. 674 | expect(microfiber.getType({ kind: KINDS.INTERFACE, name: 'MyOtherInterface' })).to.not.be.ok 675 | expect(microfiber.getType({ name: 'MyOtherInterfaceType' })).to.not.be.ok 676 | expect(microfiber.getType({ name: 'MyOtherOtherInterfaceType' })).to.not.be.ok 677 | 678 | // 679 | // 680 | //******************************* 681 | 682 | 683 | //******************************* 684 | // 685 | // Stuff regarding UNION usages 686 | // 687 | 688 | expect(microfiber.getType({ kind: KINDS.UNION, name: 'MyUnion' })).to.be.ok 689 | expect(microfiber.getType({ name: 'MyUnionType' })).to.be.ok 690 | expect(microfiber.getType({ name: 'MyOtherUnionType' })).to.be.ok 691 | 692 | // Remove the query that uses MyOtherUnionType 693 | microfiber.removeQuery({ name: 'myOtherUnionType' }) 694 | 695 | expect(microfiber.getType({ kind: KINDS.UNION, name: 'MyUnion' })).to.be.ok 696 | expect(microfiber.getType({ name: 'MyUnionType' })).to.be.ok 697 | expect(microfiber.getType({ name: 'MyOtherUnionType' })).to.be.ok 698 | 699 | // Remove the query that uses MyUnion 700 | microfiber.removeQuery({ name: 'myUnions' }) 701 | 702 | expect(microfiber.getType({ kind: KINDS.UNION, name: 'MyUnion' })).to.not.be.ok 703 | expect(microfiber.getType({ name: 'MyUnionType' })).to.not.be.ok 704 | expect(microfiber.getType({ name: 'MyOtherUnionType' })).to.not.be.ok 705 | 706 | 707 | // 708 | // 709 | //******************************* 710 | 711 | // Make sure that removing the Mutation type does some things 712 | expect(findType({ kind: mutationType.kind, name: mutationType.name, response })).to.be.ok 713 | expect(response.__schema.mutationType).to.be.ok 714 | microfiber.removeType({ kind: mutationType.kind, name: mutationType.name }) 715 | response = microfiber.getResponse() 716 | expect(findType({ kind: mutationType.kind, name: mutationType.name, response })).to.not.be.ok 717 | expect(response.__schema.mutationType).to.not.be.ok 718 | }) 719 | }) 720 | 721 | 722 | function findType({ kind, name, response }) { 723 | return (response.__schema.types || []).find((type) => type.kind === kind && type.name === name) 724 | } 725 | 726 | function findFieldOnType({ typeKind, typeName, fieldName, response }) { 727 | const type = findType({ kind: typeKind, name: typeName, response }) 728 | if (!type) { 729 | return false 730 | } 731 | return (type.fields || []).find((field) => field.name === fieldName) 732 | } 733 | 734 | function findArgOnFieldOnType({ typeKind, typeName, fieldName, argName, response }) { 735 | const field = findFieldOnType({ typeKind, typeName, fieldName, response }) 736 | if (!field) { 737 | return false 738 | } 739 | return (field.args || []).find((arg) => arg.name === argName) 740 | } 741 | 742 | function findInputFieldOnInputType({ typeName, fieldName, response }) { 743 | const type = findType({ kind: KINDS.INPUT_OBJECT, name: typeName, response }) 744 | if (!type) { 745 | return false 746 | } 747 | 748 | return (type.inputFields || []).find((inputField => inputField.name === fieldName )) 749 | } 750 | --------------------------------------------------------------------------------