├── .all-contributorsrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .huskyrc.js ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── babel.config.js ├── docs └── preview.png ├── logo.svg ├── package-lock.json ├── package.json └── src ├── astToBody.js ├── constants.js ├── index.js ├── index.test.js ├── test-utils.js └── utils.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "graphql-args", 3 | "projectOwner": "smeijer", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "smeijer", 15 | "name": "Stephan Meijer", 16 | "avatar_url": "https://avatars1.githubusercontent.com/u/1196524?v=4", 17 | "profile": "https://github.com/smeijer", 18 | "contributions": [ 19 | "ideas", 20 | "code", 21 | "infra", 22 | "maintenance" 23 | ] 24 | }, 25 | ], 26 | "contributorsPerLine": 7, 27 | "skipCi": true 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | # Match SemVer major release branches, e.g. "2.x" or ".x" 7 | - '[0-9]+.x' 8 | - 'main' 9 | - 'next' 10 | - 'alpha' 11 | - 'beta' 12 | - '!all-contributors/**' 13 | pull_request: 14 | types: [opened, synchronize, reopened] 15 | 16 | jobs: 17 | setup: 18 | name: Setup 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 5 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: bahmutov/npm-install@v1 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | needs: [setup] 29 | timeout-minutes: 5 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: bahmutov/npm-install@v1 33 | - name: Test 34 | run: npm run test 35 | 36 | release: 37 | needs: [test] 38 | if: github.event_name == 'push' && github.repository == 'smeijer/graphql-args' 39 | runs-on: ubuntu-latest 40 | environment: npm 41 | steps: 42 | - name: Cancel previous runs 43 | uses: styfle/cancel-workflow-action@0.9.0 44 | 45 | - uses: actions/checkout@v4 46 | - uses: bahmutov/npm-install@v1 47 | 48 | - name: Build 49 | run: npm run build 50 | 51 | - name: Release 52 | uses: cycjimmy/semantic-release-action@v2 53 | with: 54 | semantic_version: 17 55 | branches: | 56 | [ 57 | '+([0-9])?(.{+([0-9]),x}).x', 58 | 'main', 59 | 'next', 60 | { name: 'alpha', prerelease: true }, 61 | { name: 'beta', prerelease: true } 62 | ] 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} 65 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'pre-commit': 'pretty-quick --staged', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stephan Meijer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-args 2 | 3 | 4 | 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 6 | 7 | 8 | 9 | **extract query arguments from the graphql ast** 10 | 11 | ![screenshot of getArgs output](./docs/preview.png) 12 | 13 | `graphql-args` provides a way to extract query fields and arguments from the 4th resolver argument. 14 | 15 | ## Installation 16 | 17 | With npm: 18 | 19 | ```sh 20 | npm install --save-dev graphql-args 21 | ``` 22 | 23 | With yarn: 24 | 25 | ```sh 26 | yarn add -D graphql-args 27 | ``` 28 | 29 | ## Intro 30 | 31 | The examples below use the following query: 32 | 33 | ```gql 34 | query($where: CommentFilterInput!) { 35 | blog { 36 | id 37 | title 38 | 39 | author(id: "author_1") { 40 | name 41 | } 42 | 43 | comment(where: { id: "comment_1" }) { 44 | id 45 | } 46 | 47 | comments(where: $where) { 48 | id 49 | message 50 | author 51 | 52 | likes(where: { type: "heart" }) { 53 | actor 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | with the following variables: 61 | 62 | ```js 63 | { 64 | where: { 65 | id: 'comment_2', 66 | }, 67 | } 68 | ``` 69 | 70 | `ast` is the fourth argument of graphql resolvers: 71 | 72 | ```js 73 | import { getArgs, getFields, parse } from 'graphql-args'; 74 | 75 | const resolvers = { 76 | blog(parent, args, context, ast) { 77 | /** 78 | * This is the place where the examples fit in. The 79 | * wrapping resolver code is left out from the examples 80 | * for brevity. 81 | **/ 82 | }, 83 | }; 84 | ``` 85 | 86 | ## API 87 | 88 | ### getArgs 89 | 90 | `getArgs` reads the `ast` and returns the query arguments at a given path, as plain object. 91 | 92 | ```js 93 | import { getArgs } from 'graphql-args'; 94 | ``` 95 | 96 | #### getArgs(ast, 'path'): object 97 | 98 | ```js 99 | getArgs(ast, 'comments'); 100 | » { where: { id: 'comment_2' } } 101 | ``` 102 | 103 | #### getArgs(ast, 'nested.path'): object 104 | 105 | ```js 106 | getArgs(ast, 'comments.likes'); 107 | » { where: { type: 'heart' } } 108 | ``` 109 | 110 | #### getArgs(ast): get('path'): object 111 | 112 | In the scenario where you need to get arguments from multiple paths, and don't want to parse the `ast` more than once, the curry behavior of `getArgs` can be used. 113 | 114 | ```js 115 | const get = getArgs(ast); 116 | 117 | get('comments'); 118 | » { where: { id: 'comment_2' } } 119 | 120 | get('comments.likes'); 121 | » { where: { type: 'heart' } } 122 | ``` 123 | 124 | Alternatively, `parse` can be used to achieve the same result: 125 | 126 | ```js 127 | const { getArgs } = parse(ast); 128 | 129 | getArgs('comments'); 130 | » { where: { id: 'comment_2' } } 131 | 132 | getArgs('comments.likes'); 133 | » { where: { type: 'heart' } } 134 | ``` 135 | 136 | ### getFields 137 | 138 | `getFields` reads the `ast` and returns the query document at a given path, as plain object with the property values set to `true`. The depth of the object can be controlled with the `{ depth: number }` option. 139 | 140 | ```js 141 | import { getFields } from 'graphql-args'; 142 | ``` 143 | 144 | #### getFields(ast, 'path'): object 145 | 146 | By default, a flat object is being returned, only containing the first level properties 147 | 148 | ```js 149 | getFields(ast, 'comments'); 150 | » { id: true, author: true, likes: true, message: true } 151 | ``` 152 | 153 | #### getFields(ast, 'path', { depth: number }): object 154 | 155 | By specifying a depth, we control the nesting of the fields. Set `depth` to `0` or `-1` for unlimited depth. Depth defaults to `1` level. 156 | 157 | ```js 158 | getFields(ast, 'comments', { depth: 2 }); 159 | » { id: true, author: true, likes: { actor: true }, message: true } 160 | ``` 161 | 162 | #### getFields(ast, 'nested.path'): object 163 | 164 | ```js 165 | getFields(ast, 'comments.likes'); 166 | » { actor: true } 167 | ``` 168 | 169 | #### getFields(ast): get('path'): object 170 | 171 | In the scenario where you need to get fields from multiple paths, and don't want to parse the ``ast` more than once, the curry behavior of `getFields` can be used. 172 | 173 | ```js 174 | const get = getFields(ast); 175 | 176 | get('comments'); 177 | » { id: true, author: true, likes: true, message: true } 178 | 179 | get('comments.likes'); 180 | » { actor: true } 181 | ``` 182 | 183 | Alternatively, `parse` can be used to achieve the same result: 184 | 185 | ```js 186 | const { getFields } = parse(ast); 187 | 188 | getFields('comments'); 189 | » { id: true, author: true, likes: true, message: true } 190 | 191 | getFields('comments.likes'); 192 | » { actor: true } 193 | ``` 194 | 195 | ### parse(ast) => { getArgs, getFields } 196 | 197 | In case you'd need to query multiple paths, `parse` can be used as optimization. By using `parse`, the `ast` is only processed once. This is a small optimization, and the performance gain might be negligible. Use it when you need every last bit of performance juice, or when you just like the style. 198 | 199 | Using `parse` doesn't hurt, but in most cases, using the direct methods is what you're looking for. Simply because it's less verbose. 200 | 201 | ```js 202 | import { parse } from 'graphql-args'; 203 | 204 | const { getArgs, getFields } = parse(ast); 205 | 206 | getArgs('comments'); 207 | » { where: { id: 'comment_2' } } 208 | 209 | getFields('comments', { depth: 2 }); 210 | » { id: true, author: true, likes: { actor: true }, message: true } 211 | ``` 212 | 213 | ## Prior Art 214 | 215 | Part of this library was created when I encountered a few shortcomings in [cult-of-coders/grapher](https://github.com/cult-of-coders/grapher). While waiting for my [PR](https://github.com/cult-of-coders/grapher/pull/435) to get merged, I decided to extract some code, and adjust it to my needs. 216 | 217 | ## Contributors ✨ 218 | 219 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 |

Stephan Meijer

🤔 💻 🚇 🚧
229 | 230 | 231 | 232 | 233 | 234 | 235 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 236 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = (api) => { 3 | if (!api.env('test')) { 4 | return {}; 5 | } 6 | 7 | return { 8 | presets: [ 9 | [ 10 | '@babel/preset-env', 11 | { 12 | targets: { 13 | node: 'current', 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /docs/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smeijer/graphql-args/75825573c9e61e72e9491c52657f05375ccfc6b9/docs/preview.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-args", 3 | "version": "1.4.0", 4 | "description": "A lib that parses the resolver ast, to return the requested object fields and provided params, at any nested level.", 5 | "keywords": [ 6 | "nodejs", 7 | "graphql", 8 | "mongodb" 9 | ], 10 | "source": "src/index.js", 11 | "main": "dist/index.js", 12 | "license": "MIT", 13 | "author": "Stephan Meijer ", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/smeijer/graphql-args.git" 17 | }, 18 | "scripts": { 19 | "test": "jest", 20 | "build": "rimraf ./dist && microbundle -i src/index.js -o dist/index.js --no-pkg-main -f umd --target node", 21 | "watch": "rimraf ./dist && microbundle -i src/index.js -o dist/index.js --no-pkg-main -f umd --sourcemap true --compress false --target node --watch --raw", 22 | "prettier": "prettier . --write", 23 | "prepare": "npm run build" 24 | }, 25 | "files": [ 26 | "docs", 27 | "dist", 28 | "types" 29 | ], 30 | "devDependencies": { 31 | "graphql": "^15.3.0", 32 | "husky": "^4.3.0", 33 | "jest": "^26.4.2", 34 | "microbundle": "^0.12.4", 35 | "prettier": "^2.1.2", 36 | "pretty-quick": "^3.0.2", 37 | "rimraf": "^3.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/astToBody.js: -------------------------------------------------------------------------------- 1 | import { Kind } from 'graphql'; 2 | import { Symbols } from './constants'; 3 | 4 | export function astToBody(ast) { 5 | const body = extractSelectionSet( 6 | { selections: ast.fieldNodes }, 7 | ast.variableValues, 8 | ast.fragments, 9 | ); 10 | 11 | return Object.values(body)[0]; 12 | } 13 | 14 | function extractArgumentValue(value, variables) { 15 | if (!value) return null; 16 | const kind = value.kind; 17 | switch (kind) { 18 | case Kind.VARIABLE: 19 | return variables[value.name.value]; 20 | case Kind.INT: 21 | return parseInt(value.value); 22 | case Kind.FLOAT: 23 | return parseFloat(value.value); 24 | case Kind.BOOLEAN: 25 | return Boolean(value.value); 26 | case Kind.NULL: 27 | return null; 28 | case Kind.STRING: 29 | case Kind.ENUM: 30 | return value.value; 31 | case Kind.LIST: 32 | return value.values.map((v) => extractArgumentValue(v, variables)); 33 | case Kind.OBJECT: 34 | return Object.keys(value.fields).reduce((acc, key) => { 35 | acc[value.fields[key].name.value] = extractArgumentValue( 36 | value.fields[key].value, 37 | variables, 38 | ); 39 | return acc; 40 | }, {}); 41 | default: 42 | return value.value; 43 | } 44 | } 45 | 46 | function extractArguments(args, variables) { 47 | if (!Array.isArray(args) || args.length === 0) { 48 | return {}; 49 | } 50 | 51 | // recursively walk down the ast, to collect all arguments. The recursive 52 | // behavior covers input types (arguments of type object/array etc. supports all GraphQL type). 53 | // think of queries like `comments({ where: { blog: { id: 123 } } })` 54 | return args.reduce((acc, field) => { 55 | acc[field.name.value] = extractArgumentValue(field.value, variables); 56 | return acc; 57 | }, {}); 58 | } 59 | 60 | function extractSelectionSet(set, variables, fragments) { 61 | let body = {}; 62 | 63 | set.selections.forEach((el) => { 64 | if (el.kind === 'FragmentSpread') { 65 | if (fragments === null) { 66 | return; 67 | } 68 | const fragment = fragments[el.name.value]; 69 | const selectionSet = extractSelectionSet( 70 | fragment.selectionSet, 71 | variables, 72 | fragments, 73 | ); 74 | 75 | Object.assign(body, selectionSet); 76 | } else if (!el.selectionSet) { 77 | body[el.name.value] = true; 78 | } else { 79 | body[el.name.value] = extractSelectionSet( 80 | el.selectionSet, 81 | variables, 82 | fragments, 83 | ); 84 | body[el.name.value][Symbols.ARGUMENTS] = extractArguments( 85 | el.arguments, 86 | variables, 87 | ); 88 | } 89 | }); 90 | 91 | return body; 92 | } 93 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const Symbols = { 2 | ARGUMENTS: Symbol('arguments'), 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { astToBody } from './astToBody'; 2 | import { Symbols } from './constants'; 3 | import { get, clean } from './utils'; 4 | export { getAst } from './test-utils'; 5 | 6 | export function parse(ast) { 7 | const body = astToBody(ast); 8 | 9 | return { 10 | getArgs: (path) => { 11 | const node = get(path, body); 12 | 13 | if (!node) { 14 | return {}; 15 | } 16 | 17 | return node[Symbols.ARGUMENTS] || {}; 18 | }, 19 | 20 | getFields: (path, options = {}) => { 21 | const node = get(path, body) || {}; 22 | return clean(node, options.depth); 23 | }, 24 | }; 25 | } 26 | 27 | export function getArgs(ast, path) { 28 | if (typeof path === 'undefined') { 29 | return parse(ast).getArgs; 30 | } 31 | 32 | return parse(ast).getArgs(path); 33 | } 34 | 35 | export function getFields(ast, path, options) { 36 | if (typeof path === 'undefined') { 37 | return parse(ast).getFields; 38 | } 39 | 40 | return parse(ast).getFields(path, options); 41 | } 42 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import { parse, getFields, getArgs, getAst } from './index'; 2 | import { ast, example } from './test-utils'; 3 | 4 | test('getArgs can retrieve the root arguments', () => { 5 | const args = getArgs(ast, ''); 6 | 7 | expect(args).toEqual({ 8 | id: 'blog_1', 9 | }); 10 | }); 11 | 12 | test('getArgs can retrieve the nested arguments', () => { 13 | const args = getArgs(ast, 'author'); 14 | 15 | expect(args).toEqual({ 16 | id: 'author_1', 17 | }); 18 | }); 19 | 20 | test('getArgs can retrieve the nested input type arguments', () => { 21 | const args = getArgs(ast, 'comment'); 22 | 23 | expect(args).toEqual({ 24 | where: { id: 'comment_1' }, 25 | }); 26 | }); 27 | 28 | test('getArgs can retrieve the nested input type arguments using variables', () => { 29 | const args = getArgs(ast, 'comments'); 30 | 31 | expect(args).toEqual({ 32 | where: { id: 'comment_2' }, 33 | }); 34 | }); 35 | 36 | test('getArgs can retrieve the deeply nested arguments', () => { 37 | const args = getArgs(ast, 'comments.likes'); 38 | 39 | expect(args).toEqual({ 40 | where: { type: 'heart' }, 41 | }); 42 | }); 43 | 44 | test('getArgs can use `parse` as optimization', () => { 45 | const { getArgs } = parse(ast); 46 | const args = getArgs('comment'); 47 | 48 | expect(args).toEqual({ 49 | where: { id: 'comment_1' }, 50 | }); 51 | }); 52 | 53 | test('getArgs returns a creator instance when called with single argument', () => { 54 | const get = getArgs(ast); 55 | const args = get('comment'); 56 | 57 | expect(args).toEqual({ 58 | where: { id: 'comment_1' }, 59 | }); 60 | }); 61 | 62 | test('getArgs can retrieve the mixed arguments', () => { 63 | const args = getArgs(ast, 'mixed'); 64 | 65 | expect(args).toEqual({ 66 | where: { 67 | e: 'App', 68 | w: { id: 'comment_2' }, 69 | id: 'mixed_id', 70 | args: [1, true, 'true', null, { a: 'b' }], 71 | x: { a: { b: 'c' } }, 72 | array: [1, 2, 3, 4, 3.14], 73 | }, 74 | }); 75 | }); 76 | 77 | test('getFields can return the root level', () => { 78 | const fields = getFields(ast, ''); 79 | 80 | expect(fields).toMatchObject({ 81 | author: true, 82 | comment: true, 83 | comments: true, 84 | id: true, 85 | title: true, 86 | }); 87 | }); 88 | 89 | test('getFields can limit the depth', () => { 90 | const fields = getFields(ast, '', { depth: 2 }); 91 | 92 | expect(fields).toMatchObject({ 93 | author: { name: true }, 94 | comment: { id: true }, 95 | comments: { 96 | author: true, 97 | id: true, 98 | // likes is flattened, original has { actor: true } 99 | likes: true, 100 | message: true, 101 | }, 102 | id: true, 103 | title: true, 104 | }); 105 | }); 106 | 107 | test('getFields can return the entire object', () => { 108 | const fields = getFields(ast, '', { depth: -1 }); 109 | 110 | expect(fields).toMatchObject({ 111 | author: { name: true }, 112 | comment: { id: true }, 113 | comments: { 114 | author: true, 115 | id: true, 116 | likes: { actor: { email: true, name: true } }, 117 | message: true, 118 | }, 119 | id: true, 120 | title: true, 121 | }); 122 | }); 123 | 124 | test('getFields can retrieve nested objects', () => { 125 | const fields = getFields(ast, 'author'); 126 | 127 | expect(fields).toMatchObject({ 128 | name: true, 129 | }); 130 | }); 131 | 132 | test('getFields can retrieve the deeply nested objects', () => { 133 | const fields = getFields(ast, 'comments.likes'); 134 | 135 | expect(fields).toEqual({ 136 | actor: true, 137 | }); 138 | }); 139 | 140 | test('getFields can use `parse` as optimization', () => { 141 | const { getFields } = parse(ast); 142 | const fields = getFields('comments'); 143 | 144 | expect(fields).toEqual({ 145 | id: true, 146 | author: true, 147 | likes: true, 148 | message: true, 149 | }); 150 | }); 151 | 152 | test('getFields creator instance correctly handles nested objects with depth', () => { 153 | const { getFields } = parse(ast); 154 | const fields = getFields('comments', { depth: 2 }); 155 | 156 | expect(fields).toEqual({ 157 | id: true, 158 | author: true, 159 | likes: { actor: true }, 160 | message: true, 161 | }); 162 | }); 163 | 164 | describe('AppSync environment', () => { 165 | test('It should create an ast without fragments', () => { 166 | expect(getAst(example, true).fragments).toBe(null); 167 | }); 168 | test('It should should ignore fragments', () => { 169 | const { getFields } = parse(getAst(example, true)); 170 | const fields = getFields('comments.likes', { depth: -1 }); 171 | expect(fields).toEqual({ 172 | actor: {}, 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /src/test-utils.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql'; 2 | 3 | export function getAst({ query, variables }, isAppSyncSelectionSet = false) { 4 | const ast = parse(query); 5 | 6 | const operation = ast.definitions.find( 7 | (x) => x.kind === 'OperationDefinition', 8 | ); 9 | 10 | const fragments = ast.definitions 11 | .filter((x) => x.kind === 'FragmentDefinition') 12 | .reduce((acc, fragment) => { 13 | acc[fragment.name.value] = fragment; 14 | return acc; 15 | }, {}); 16 | 17 | return { 18 | fieldNodes: operation.selectionSet.selections, 19 | variableValues: variables, 20 | fragments: !isAppSyncSelectionSet ? fragments : null, 21 | }; 22 | } 23 | 24 | export const example = { 25 | query: /* GraphQL */ ` 26 | query($where: CommentFilterInput!) { 27 | blog(id: "blog_1") { 28 | id 29 | title 30 | 31 | author(id: "author_1") { 32 | name 33 | } 34 | 35 | comment(where: { id: "comment_1" }) { 36 | id 37 | } 38 | 39 | comments(where: $where) { 40 | id 41 | message 42 | author 43 | 44 | likes(where: { type: "heart" }) { 45 | actor { 46 | ...Person 47 | } 48 | } 49 | } 50 | mixed( 51 | where: { 52 | e: App 53 | w: $where 54 | id: "mixed_id" 55 | args: [1, true, "true", null, { a: "b" }] 56 | x: { a: { b: "c" } } 57 | array: [1, 2, 3, 4, 3.14] 58 | } 59 | ) { 60 | id 61 | name 62 | } 63 | } 64 | } 65 | 66 | fragment Person on Actor { 67 | name 68 | email 69 | } 70 | `, 71 | variables: { 72 | where: { 73 | id: 'comment_2', 74 | }, 75 | }, 76 | }; 77 | 78 | export const ast = getAst(example); 79 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function get(path, object) { 2 | if (!path) { 3 | return object; 4 | } 5 | 6 | const parts = Array.isArray(path) ? path : path.split('.'); 7 | 8 | while (object && parts.length > 0) { 9 | object = object[parts.shift()]; 10 | } 11 | 12 | if (!object) { 13 | return undefined; 14 | } 15 | 16 | return object; 17 | } 18 | 19 | export function clean(obj, maxDepth = 1, depth = 1) { 20 | if (Array.isArray(obj)) { 21 | return obj.map((item) => clean(item, maxDepth, depth + 1)); 22 | } 23 | 24 | if (typeof obj === 'object') { 25 | // symbols are excluded from Object.keys({}) 26 | return Object.keys(obj).reduce((acc, key) => { 27 | if (key === '__typename') { 28 | return acc; 29 | } 30 | 31 | acc[key] = 32 | depth < maxDepth || maxDepth < 1 33 | ? clean(obj[key], maxDepth, depth + 1) 34 | : true; 35 | 36 | return acc; 37 | }, {}); 38 | } 39 | 40 | return obj; 41 | } 42 | --------------------------------------------------------------------------------