├── .github └── workflows │ ├── ci.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── CURRENT_LIMITATIONS.md ├── LICENSE ├── README.md ├── __tests__ ├── .gitkeep ├── README.md └── starwars.schema.graphql ├── codegen ├── __tests__ │ └── .gitkeep ├── bin │ └── index ├── package.json ├── src │ ├── CLI.ts │ ├── __tests__ │ │ └── render.test.ts │ ├── index.ts │ ├── printers.ts │ ├── render.ts │ ├── transforms │ │ ├── index.ts │ │ ├── selectors.ts │ │ └── types.ts │ └── utils.ts ├── tsconfig.json └── tsconfig.release.json ├── docs ├── api │ └── selection.md ├── client-examples │ ├── apollo.md │ ├── graphql-request.md │ └── urql.md ├── directives.md ├── fragments │ ├── inline.md │ └── named.md ├── operations.md ├── quickstart │ ├── codegen.md │ ├── demo.md │ └── installation.md └── variables.md ├── dprint.json ├── examples ├── .gitkeep ├── 01-query.ts ├── 02-mutation.ts ├── 03-fragments.ts └── generated.ts ├── index.d.ts ├── jest.config.js ├── lefthook.yml ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── AST.ts ├── Result.ts ├── Selection.ts ├── Variables.ts ├── __tests__ │ ├── .gitkeep │ ├── Selection.test.ts │ └── Variables.test.ts └── index.ts ├── test-d ├── field-alias.test-d.ts ├── fragments.test-d.ts ├── interface.test-d.ts ├── nested-objects.ts ├── nested-variables.test-d.ts ├── optional-variables.test-d.ts ├── parameterized.test-d.ts ├── simple-object.ts ├── simple-scalar.test-d.ts ├── union.test-d.ts ├── variables-nested-in-fragments.test-d.ts └── variables.test-d.ts ├── tsconfig.json ├── tsconfig.release.json └── website ├── .gitignore ├── README.md ├── babel.config.js ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src ├── components │ ├── HomepageFeatures.js │ └── HomepageFeatures.module.css ├── css │ └── custom.css └── pages │ ├── index.js │ ├── index.module.css │ └── markdown-page.md └── static ├── .nojekyll └── img ├── docusaurus.png ├── favicon.ico ├── logo.svg ├── tutorial ├── docsVersionDropdown.png └── localeDropdown.png ├── undraw_docusaurus_mountain.svg ├── undraw_docusaurus_react.svg └── undraw_docusaurus_tree.svg /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Run basic CI processes like building, linting, testing 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: 'Checkout' 17 | uses: actions/checkout@master 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '16' 23 | 24 | - name: Cache pnpm modules 25 | uses: actions/cache@v2 26 | with: 27 | path: ~/.pnpm-store 28 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 29 | restore-keys: | 30 | ${{ runner.os }}- 31 | 32 | - uses: pnpm/action-setup@v2.0.1 33 | with: 34 | version: 6.0.2 35 | run_install: true 36 | 37 | - name: 'Build' 38 | run: pnpm build:release 39 | 40 | - name: 'Type Test' 41 | run: pnpm type-test 42 | 43 | - name: 'Test' 44 | run: pnpm test -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: npm-publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '16' 19 | 20 | - name: Cache pnpm modules 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.pnpm-store 24 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 25 | restore-keys: | 26 | ${{ runner.os }}- 27 | 28 | - uses: pnpm/action-setup@v2.0.1 29 | with: 30 | version: 6.0.2 31 | run_install: true 32 | 33 | - name: 'Build' 34 | run: pnpm build:release 35 | 36 | - name: 'Test' 37 | run: pnpm test 38 | 39 | - name: 'NPM Publish' 40 | uses: JS-DevTools/npm-publish@v1 41 | with: 42 | token: ${{ secrets.NPM_TOKEN }} 43 | access: "public" 44 | check-version: true 45 | 46 | - name: 'GitHub Publish' 47 | uses: JS-DevTools/npm-publish@v1 48 | with: 49 | registry: https://npm.pkg.github.com 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | access: "public" 52 | check-version: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | # quick way to fetch all commits so our changelog is generated correctly 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '16' 23 | 24 | - uses: olegtarasov/get-tag@v2.1 25 | id: tagName 26 | 27 | - name: Generate Release Notes 28 | run: npx auto-changelog --starting-version ${{ steps.tagName.outputs.tag }} --output RELEASE_NOTES.md 29 | 30 | - name: Create Release 31 | id: create_release 32 | uses: actions/create-release@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | tag_name: ${{ github.ref }} 37 | release_name: ${{ github.ref }} 38 | body_path: RELEASE_NOTES.md 39 | draft: false 40 | prerelease: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | notes.txt 107 | 108 | failing-examples 109 | 110 | sdk.ts 111 | *_test.ts -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [v1.0.0-rc.10](https://github.com/timkendall/tql/compare/v1.0.0-rc.9...v1.0.0-rc.10) 8 | 9 | > 28 July 2022 10 | 11 | - Update yargs and accept output file path in CLI [`#116`](https://github.com/timkendall/tql/pull/116) 12 | - Bump to 1.0.0-rc.10 [`822fc17`](https://github.com/timkendall/tql/commit/822fc1749ae23b5d24022d1c016e29687e9f9db7) 13 | 14 | #### [v1.0.0-rc.9](https://github.com/timkendall/tql/compare/v1.0.0-rc.8...v1.0.0-rc.9) 15 | 16 | > 4 June 2022 17 | 18 | - Add support to pass http headers when running codegen cli [`#113`](https://github.com/timkendall/tql/pull/113) 19 | - Bump to 1.0.0-rc.9 [`05a8117`](https://github.com/timkendall/tql/commit/05a811790c144f8a705e731cb47f67930b7286eb) 20 | 21 | #### [v1.0.0-rc.8](https://github.com/timkendall/tql/compare/v1.0.0-rc.7...v1.0.0-rc.8) 22 | 23 | > 29 April 2022 24 | 25 | - Field alias support [`#110`](https://github.com/timkendall/tql/pull/110) 26 | - Bump to 1.0.0-rc.8 [`87ec936`](https://github.com/timkendall/tql/commit/87ec9364e0fb3ec0d9c1a5051445d045503812ee) 27 | 28 | #### [v1.0.0-rc.7](https://github.com/timkendall/tql/compare/v1.0.0-rc.6...v1.0.0-rc.7) 29 | 30 | > 28 April 2022 31 | 32 | - Add SelectionResult type to make type alias extraction easier [`#111`](https://github.com/timkendall/tql/pull/111) 33 | - Bump to 1.0.0-rc.7 [`f253d17`](https://github.com/timkendall/tql/commit/f253d177cadec48e1183d8715fd6b2ec9a6f5876) 34 | 35 | #### [v1.0.0-rc.6](https://github.com/timkendall/tql/compare/v1.0.0-rc.5...v1.0.0-rc.6) 36 | 37 | > 15 April 2022 38 | 39 | - Add support for latest graphql-js [`#108`](https://github.com/timkendall/tql/pull/108) 40 | - Faster codeformatter [`#94`](https://github.com/timkendall/tql/pull/94) 41 | - Add examples of basic usage [`e31ecb6`](https://github.com/timkendall/tql/commit/e31ecb608a3a675fe8214f356e880046307833da) 42 | - Upgrade to Jest 27 [`7bcde9b`](https://github.com/timkendall/tql/commit/7bcde9bb4c89d0f59c95166062cc703aa4421b2d) 43 | - Upgrade to typescript@4.5.4 [`f9071b5`](https://github.com/timkendall/tql/commit/f9071b552b0192dad4fefb13023a97961710b99b) 44 | 45 | #### [v1.0.0-rc.5](https://github.com/timkendall/tql/compare/v1.0.0-rc.4...v1.0.0-rc.5) 46 | 47 | > 2 January 2022 48 | 49 | - Properly print list scalar input types (closes #93) [`#93`](https://github.com/timkendall/tql/issues/93) 50 | - Don't require schema definition in SDL for codegen (closes #91) [`#91`](https://github.com/timkendall/tql/issues/91) 51 | - Strip directive definitions from codegen output (closes #92) [`#92`](https://github.com/timkendall/tql/issues/92) 52 | 53 | #### [v1.0.0-rc.4](https://github.com/timkendall/tql/compare/v1.0.0-rc.3...v1.0.0-rc.4) 54 | 55 | > 11 December 2021 56 | 57 | - Fix buildVariableDefinitions to use proper root type [`30c3114`](https://github.com/timkendall/tql/commit/30c311440839b1f22f7132eb136f932fbdc21b32) 58 | - Bump to 1.0.0-rc.4 [`08bdb62`](https://github.com/timkendall/tql/commit/08bdb620a64e0f0c0985f21ba15c192897132711) 59 | - Bump to 1.0.0-rc.3 [`a40c729`](https://github.com/timkendall/tql/commit/a40c72915298a5bff76a3178960d6b61c25ff0a7) 60 | 61 | #### [v1.0.0-rc.3](https://github.com/timkendall/tql/compare/v1.0.0-rc.2...v1.0.0-rc.3) 62 | 63 | > 9 December 2021 64 | 65 | - Configure website google analytics [`#90`](https://github.com/timkendall/tql/pull/90) 66 | - Create docs/ and website/ [`#89`](https://github.com/timkendall/tql/pull/89) 67 | - Use PNPM in CI workflows [`61f15c1`](https://github.com/timkendall/tql/commit/61f15c1e28364bafc31cbf53a1e3d6542751e746) 68 | - Fix buildVariableDefinitions [`36d5abb`](https://github.com/timkendall/tql/commit/36d5abbca095b1153fd171709e191f0718e590bc) 69 | - Update current limitations doc [`4e77c4e`](https://github.com/timkendall/tql/commit/4e77c4ee64b36d508b206a0b999a5ca1ffb57392) 70 | 71 | #### [v1.0.0-rc.2](https://github.com/timkendall/tql/compare/v1.0.0-rc.1...v1.0.0-rc.2) 72 | 73 | > 25 November 2021 74 | 75 | - Highlight core features in README [`ae2934f`](https://github.com/timkendall/tql/commit/ae2934fdd1a6faa89df3e02e46696c96b74aa4d9) 76 | - Correctly build GraphQLSchema object in generated SDK [`6813a65`](https://github.com/timkendall/tql/commit/6813a65395bacf02d1a81df50f91317ef3dec1ca) 77 | - Exclude test files from release builds [`1b56242`](https://github.com/timkendall/tql/commit/1b56242cd55d2248830a82b4e9dd37cd085d121e) 78 | 79 | #### [v1.0.0-rc.1](https://github.com/timkendall/tql/compare/v0.8.0...v1.0.0-rc.1) 80 | 81 | > 25 November 2021 82 | 83 | - Ast rewrite [`#88`](https://github.com/timkendall/tql/pull/88) 84 | 85 | #### [v0.8.0](https://github.com/timkendall/tql/compare/v0.7.1...v0.8.0) 86 | 87 | > 23 October 2021 88 | 89 | - Bump ansi-regex from 5.0.0 to 5.0.1 [`#74`](https://github.com/timkendall/tql/pull/74) 90 | - Bump tmpl from 1.0.4 to 1.0.5 [`#73`](https://github.com/timkendall/tql/pull/73) 91 | - Bump path-parse from 1.0.6 to 1.0.7 [`#61`](https://github.com/timkendall/tql/pull/61) 92 | - Patch nested enum input bug (closes #62) [`#62`](https://github.com/timkendall/tql/issues/62) 93 | - Add package.json keywords (closes #60) [`#60`](https://github.com/timkendall/tql/issues/60) 94 | - Implement Selector and selector APIs [`d5fb12f`](https://github.com/timkendall/tql/commit/d5fb12f338554b6f42982da223adb396cd3d78d2) 95 | - Extract Result type into module [`fbb6313`](https://github.com/timkendall/tql/commit/fbb6313fffcdd8079ab548e3023fd2821e119700) 96 | - Add Operation.sha256 [`c0f2dc7`](https://github.com/timkendall/tql/commit/c0f2dc703cbbb0543b9d87ba10fdfde22298364d) 97 | 98 | #### [v0.7.1](https://github.com/timkendall/tql/compare/v0.7.0...v0.7.1) 99 | 100 | > 3 August 2021 101 | 102 | - Add missing @apollo/client devDependency [`d90b2c0`](https://github.com/timkendall/tql/commit/d90b2c07b7bc15b6e91f91b7c807d8596aa81c15) 103 | - Add missing graphql-request devDependency [`50116ea`](https://github.com/timkendall/tql/commit/50116ea6c71b537407b9d27533c1f50356e5d7d1) 104 | 105 | #### [v0.7.0](https://github.com/timkendall/tql/compare/v0.6.0...v0.7.0) 106 | 107 | > 3 August 2021 108 | 109 | - Add graphql-request example (closes #17) [`#17`](https://github.com/timkendall/tql/issues/17) 110 | - Improve apollo-client example [`bfbce55`](https://github.com/timkendall/tql/commit/bfbce55f069d9123210854f17acec582dab7efe3) 111 | - Allow Executor to take custom fetch and httpHeaders [`04b96b8`](https://github.com/timkendall/tql/commit/04b96b8ac9720e303da470f639aa7cc66f9ad3fc) 112 | - Remove Repl.it disclaimer about TypeScript 4.1 [`1a42fd8`](https://github.com/timkendall/tql/commit/1a42fd870ebff7f25e3485d45793e77e5f952b0d) 113 | 114 | #### [v0.6.0](https://github.com/timkendall/tql/compare/v0.5.4...v0.6.0) 115 | 116 | > 23 June 2021 117 | 118 | - Bump ws from 7.4.1 to 7.4.6 [`#55`](https://github.com/timkendall/tql/pull/55) 119 | - Bump handlebars from 4.7.6 to 4.7.7 [`#51`](https://github.com/timkendall/tql/pull/51) 120 | - Bump hosted-git-info from 2.8.8 to 2.8.9 [`#52`](https://github.com/timkendall/tql/pull/52) 121 | - Bump lodash from 4.17.20 to 4.17.21 [`#53`](https://github.com/timkendall/tql/pull/53) 122 | - Unwrap results in client methods [`2f14b43`](https://github.com/timkendall/tql/commit/2f14b43c06dec2c7bb2b4ab38fa48d9cb89baeb0) 123 | - Fix scalar and enum root-level client fields [`9575864`](https://github.com/timkendall/tql/commit/95758648d3738790009b697dc0bd527a4c10ca31) 124 | - Fix ReadonlyArray field being inferred as ReadonlyObject [`8675820`](https://github.com/timkendall/tql/commit/8675820f0d0d549e1baae99850498b3b6eaa15a8) 125 | 126 | #### [v0.5.4](https://github.com/timkendall/tql/compare/v0.5.3...v0.5.4) 127 | 128 | > 2 April 2021 129 | 130 | #### [v0.5.3](https://github.com/timkendall/tql/compare/v0.5.2...v0.5.3) 131 | 132 | > 2 April 2021 133 | 134 | - Workaround for TypeScript 4.2+ support [`d8beca8`](https://github.com/timkendall/tql/commit/d8beca8fc59ff1f2cd384dcdbc3e71bdee36d6e8) 135 | - Use JavaScript crypto implementation [`5ca4aba`](https://github.com/timkendall/tql/commit/5ca4aba042f3935079fbfe09343881da830de4ec) 136 | 137 | #### [v0.5.2](https://github.com/timkendall/tql/compare/v0.5.1...v0.5.2) 138 | 139 | > 1 March 2021 140 | 141 | - Revert "Tweak Executor interface to work with TypeScript 4.2 (#49)" [`fe5bb68`](https://github.com/timkendall/tql/commit/fe5bb6857fa25d9c044ca896a3993d813203b71d) 142 | - Revert "0.5.1" [`892f521`](https://github.com/timkendall/tql/commit/892f521c3d2fcb211bdc8b7c7fcc5f3a18a218a3) 143 | 144 | #### [v0.5.1](https://github.com/timkendall/tql/compare/v0.5.0...v0.5.1) 145 | 146 | > 1 March 2021 147 | 148 | - Tweak Executor interface to work with TypeScript 4.2 [`#49`](https://github.com/timkendall/tql/pull/49) 149 | 150 | #### [v0.5.0](https://github.com/timkendall/tql/compare/v0.4.1...v0.5.0) 151 | 152 | > 18 February 2021 153 | 154 | - Support non-standard root type names (closes #46) [`#48`](https://github.com/timkendall/tql/pull/48) 155 | - Fix typo [`#45`](https://github.com/timkendall/tql/pull/45) 156 | - Support non-standard root type names (closes #46) (#48) [`#46`](https://github.com/timkendall/tql/issues/46) 157 | - Use correct tag name value in release workflow [`3d4f105`](https://github.com/timkendall/tql/commit/3d4f105358da2488a9818bfaae4166492ed64ae9) 158 | 159 | #### [v0.4.1](https://github.com/timkendall/tql/compare/v0.4.0...v0.4.1) 160 | 161 | > 15 February 2021 162 | 163 | - Define and throw TypeConditionError on invalid runtime selection [`#44`](https://github.com/timkendall/tql/pull/44) 164 | - Add prepublish command [`a62ab02`](https://github.com/timkendall/tql/commit/a62ab023d04a23c7d910f1a619d58aeef9c64772) 165 | 166 | #### [v0.4.0](https://github.com/timkendall/tql/compare/v0.3.0...v0.4.0) 167 | 168 | > 15 February 2021 169 | 170 | - Generate deep readonly interfaces and results by default [`#41`](https://github.com/timkendall/tql/pull/41) 171 | 172 | #### [v0.3.0](https://github.com/timkendall/tql/compare/v0.2.1...v0.3.0) 173 | 174 | > 14 January 2021 175 | 176 | - Partially fix union type inference [`#35`](https://github.com/timkendall/tql/pull/35) 177 | - Remove codeql workflow [`53f8876`](https://github.com/timkendall/tql/commit/53f8876fa509d3f6d86852e9d1642cfad383d734) 178 | - Add --silent usage to README example [`219bb74`](https://github.com/timkendall/tql/commit/219bb74961286bd9210ae95e3f98e7a2260ec9bb) 179 | 180 | #### [v0.2.1](https://github.com/timkendall/tql/compare/v0.2.0...v0.2.1) 181 | 182 | > 29 December 2020 183 | 184 | #### [v0.2.0](https://github.com/timkendall/tql/compare/v0.1.1...v0.2.0) 185 | 186 | > 29 December 2020 187 | 188 | - Add nullable field support [`#26`](https://github.com/timkendall/tql/pull/26) 189 | 190 | #### [v0.1.1](https://github.com/timkendall/tql/compare/v0.1.0...v0.1.1) 191 | 192 | > 28 December 2020 193 | 194 | - Add small apollo-client example [`6b80f91`](https://github.com/timkendall/tql/commit/6b80f912467ea6f7f4ae18f3baaa7d96fe5f401f) 195 | - Render all variables correctly [`6dc8010`](https://github.com/timkendall/tql/commit/6dc8010b91630652b96c57590c2c64cb1def9896) 196 | 197 | #### [v0.1.0](https://github.com/timkendall/tql/compare/v0.1.0-alpha.5...v0.1.0) 198 | 199 | > 27 December 2020 200 | 201 | - Add npm publish workflow [`b98a96a`](https://github.com/timkendall/tql/commit/b98a96a6ebdf67cd36173efc3ae34933b970fc53) 202 | - Update workflows [`eb92990`](https://github.com/timkendall/tql/commit/eb92990e5420ef60476072debfdd87a178d2335d) 203 | - Fix yml [`8b5298b`](https://github.com/timkendall/tql/commit/8b5298b6275e4cd9b34290809d7bf3d1079089ef) 204 | 205 | #### v0.1.0-alpha.5 206 | 207 | > 27 December 2020 208 | 209 | - Support Client class codegen [`#22`](https://github.com/timkendall/tql/pull/22) 210 | - Bump node-notifier from 8.0.0 to 8.0.1 [`#15`](https://github.com/timkendall/tql/pull/15) 211 | - Create code-analysis workflow [`#16`](https://github.com/timkendall/tql/pull/16) 212 | - Experimental inline fragments [`#11`](https://github.com/timkendall/tql/pull/11) 213 | - Partially support nullable and list types [`#8`](https://github.com/timkendall/tql/pull/8) 214 | - Setup GitHub Actions CI workflow (closes #6) [`#6`](https://github.com/timkendall/tql/issues/6) 215 | - Add back missing deprecated comment; ad more tests [`2f37495`](https://github.com/timkendall/tql/commit/2f374950e95ec7066537d19b94ee65117336cc4e) 216 | - Add JSDoc description comment support [`bf9ff2d`](https://github.com/timkendall/tql/commit/bf9ff2de88e00de857fefd61e6f4181f1e857ffb) 217 | - Setup TypeScript project [`aad0c7c`](https://github.com/timkendall/tql/commit/aad0c7c26d63b05061b46c9e0d20adcac29a5c01) 218 | -------------------------------------------------------------------------------- /CURRENT_LIMITATIONS.md: -------------------------------------------------------------------------------- 1 | # Current Limitations 2 | 3 | - [`const` assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) required for abstract type selections 4 | - No named fragment support 5 | - No client directive support 6 | - No nested input object variable support -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim Kendall 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 | > **Warning**: 2 | > This library will not be recieving updates any time soon. 3 | 4 | 5 | > 🙏 Thank you to all contributors. I am not interested in maintaining this right now but please feel free to fork and take inspiration from! 6 | 7 | 8 | # TQL 9 | 10 | **tql** is a TypeScript GraphQL query builder. 11 | 12 | - 🔒 **Fully Type-safe** - Operation results and variables are fully type-safe thanks to TypeScript's advanced type-system. 13 | - 🔌 **Backendless**: - Integrate with any GraphQL client to execute queries. 14 | - 🔮 **Automatic Variables**: - Variable definitions are automatically derived based on usage. 15 | - 📝 **Inline Documentation**: JSDoc comments provide descriptions and deprecation warnings for fields directly in your editor. 16 | - ⚡ **Single Dependency**: [`graphql-js`](https://github.com/graphql/graphql-js) is our single runtime (peer) dependency. 17 | 18 | ## [Try it Out](https://codesandbox.io/s/tql-starwars-wlfg9?file=/src/index.ts&runonclick=1) 19 | 20 | Try out our pre-compiled Star Wars GraphQL SDK on [CodeSandbox](https://codesandbox.io/s/tql-starwars-wlfg9?file=/src/index.ts&runonclick=1)! 21 | 22 | ## Installation 23 | 24 | 1. `npm install @timkendall/tql@beta` 25 | 26 | * **TypeScript 4.1+** is required for [Recursive Conditional Type](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#recursive-conditional-types) support 27 | 28 | 2. Generate an SDK with `npx @timkendall/tql-gen -o sdk.ts` 29 | 30 | `` can be a path to local file or an http endpoint url. 31 | 32 | ## Usage 33 | 34 | Import selector functions to start defining queries 🎉 35 | 36 | ```typescript 37 | import { useQuery } from '@apollo/client' 38 | 39 | // SDK generated in previous setup 40 | import { character, query, $ } from './starwars' 41 | 42 | // define reusable selections 43 | const CHARACTER = character(t => [ 44 | t.id(), 45 | t.name(), 46 | t.appearsIn(), 47 | ]) 48 | 49 | const QUERY = query((t) => [ 50 | t.reviews({ episode: Episode.EMPIRE }, (t) => [ 51 | t.stars(), 52 | t.commentary(), 53 | ]), 54 | 55 | t.human({ id: $('id') }, (t) => [ 56 | t.__typename(), 57 | t.id(), 58 | t.name(), 59 | t.appearsIn(), 60 | t.homePlanet(), 61 | 62 | // deprecated field should be properly picked-up by your editor 63 | t.mass(), 64 | 65 | t.friends((t) => [ 66 | t.__typename(), 67 | 68 | ...CHARACTER, 69 | // or 70 | CHARACTER.toInlineFragment(), 71 | 72 | t.on("Human", (t) => [t.homePlanet()]), 73 | t.on("Droid", (t) => [t.primaryFunction()]), 74 | ]), 75 | 76 | t.starships((t) => [t.id(), t.name()]), 77 | ]), 78 | ]).toQuery({ name: 'Example' }) 79 | 80 | // type-safe result and variables 👍 81 | const { data } = useQuery(QUERY, { variables: { id: '1011' }}) 82 | 83 | ``` 84 | 85 | ## Inspiration 86 | 87 | I was inspired by the features and DSL's of [graphql-nexus](https://github.com/graphql-nexus/schema), [graphql_ppx](https://github.com/mhallin/graphql_ppx), [gqless](https://github.com/gqless/gqless), and [caliban](https://github.com/ghostdogpr/caliban). 88 | 89 | ## License 90 | 91 | MIT 92 | -------------------------------------------------------------------------------- /__tests__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/__tests__/.gitkeep -------------------------------------------------------------------------------- /__tests__/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | We use the canonical [graphql/swapi-graphql](https://github.com/graphql/swapi-graphql) API as our top-level integration tests. You can configure the test suite to run against your own schema by following the instructions below. If you find a bug please [open an issue](https://github.com/timkendall/tql/issues/new)!. 4 | 5 | ## Running 6 | 7 | `yarn test:int ` (defaults to the Starwars schema defined here) -------------------------------------------------------------------------------- /__tests__/starwars.schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | """The query type, represents all of the entry points into our object graph""" 6 | type Query { 7 | hero(episode: Episode): Character 8 | reviews(episode: Episode!): [Review] 9 | search(text: String): [SearchResult] 10 | character(id: ID!): Character 11 | droid(id: ID!): Droid 12 | human(id: ID!): Human 13 | starship(id: ID!): Starship 14 | } 15 | """The mutation type, represents all updates we can make to our data""" 16 | type Mutation { 17 | createReview(episode: Episode, review: ReviewInput!): Review 18 | } 19 | """The episodes in the Star Wars trilogy""" 20 | enum Episode { 21 | """Star Wars Episode IV: A New Hope, released in 1977.""" 22 | NEWHOPE 23 | """Star Wars Episode V: The Empire Strikes Back, released in 1980.""" 24 | EMPIRE 25 | """Star Wars Episode VI: Return of the Jedi, released in 1983.""" 26 | JEDI 27 | } 28 | """A character from the Star Wars universe""" 29 | interface Character { 30 | """The ID of the character""" 31 | id: ID! 32 | """The name of the character""" 33 | name: String! 34 | """The friends of the character, or an empty list if they have none""" 35 | friends: [Character] 36 | """The friends of the character exposed as a connection with edges""" 37 | friendsConnection(first: Int, after: ID): FriendsConnection! 38 | """The movies this character appears in""" 39 | appearsIn: [Episode]! 40 | } 41 | """Units of height""" 42 | enum LengthUnit { 43 | """The standard unit around the world""" 44 | METER 45 | """Primarily used in the United States""" 46 | FOOT 47 | """Ancient unit used during the Middle Ages""" 48 | CUBIT @deprecated(reason: "Test deprecated enum case") 49 | } 50 | """A humanoid creature from the Star Wars universe""" 51 | type Human implements Character { 52 | """The ID of the human""" 53 | id: ID! 54 | """What this human calls themselves""" 55 | name: String! 56 | """The home planet of the human, or null if unknown""" 57 | homePlanet: String 58 | """Height in the preferred unit, default is meters""" 59 | height(unit: LengthUnit = METER): Float 60 | """Mass in kilograms, or null if unknown""" 61 | mass: Float @deprecated(reason: "Weight is a sensitive subject!") 62 | """This human's friends, or an empty list if they have none""" 63 | friends: [Character] 64 | """The friends of the human exposed as a connection with edges""" 65 | friendsConnection(first: Int, after: ID): FriendsConnection! 66 | """The movies this human appears in""" 67 | appearsIn: [Episode]! 68 | """A list of starships this person has piloted, or an empty list if none""" 69 | starships: [Starship] 70 | } 71 | """An autonomous mechanical character in the Star Wars universe""" 72 | type Droid implements Character { 73 | """The ID of the droid""" 74 | id: ID! 75 | """What others call this droid""" 76 | name: String! 77 | """This droid's friends, or an empty list if they have none""" 78 | friends: [Character] 79 | """The friends of the droid exposed as a connection with edges""" 80 | friendsConnection(first: Int, after: ID): FriendsConnection! 81 | """The movies this droid appears in""" 82 | appearsIn: [Episode]! 83 | """This droid's primary function""" 84 | primaryFunction: String 85 | } 86 | """A connection object for a character's friends""" 87 | type FriendsConnection { 88 | """The total number of friends""" 89 | totalCount: Int 90 | """The edges for each of the character's friends.""" 91 | edges: [FriendsEdge] 92 | """A list of the friends, as a convenience when edges are not needed.""" 93 | friends: [Character] 94 | """Information for paginating this connection""" 95 | pageInfo: PageInfo! 96 | } 97 | """An edge object for a character's friends""" 98 | type FriendsEdge { 99 | """A cursor used for pagination""" 100 | cursor: ID! 101 | """The character represented by this friendship edge""" 102 | node: Character 103 | } 104 | """Information for paginating this connection""" 105 | type PageInfo { 106 | startCursor: ID 107 | endCursor: ID 108 | hasNextPage: Boolean! 109 | } 110 | """Represents a review for a movie""" 111 | type Review { 112 | """The number of stars this review gave, 1-5""" 113 | stars: Int! 114 | """Comment about the movie""" 115 | commentary: String 116 | } 117 | """The input object sent when someone is creating a new review""" 118 | input ReviewInput { 119 | """0-5 stars""" 120 | stars: Int! 121 | """Comment about the movie, optional""" 122 | commentary: String 123 | """Favorite color, optional""" 124 | favorite_color: ColorInput 125 | } 126 | """The input object sent when passing in a color""" 127 | input ColorInput { 128 | red: Int! 129 | green: Int! 130 | blue: Int! 131 | } 132 | type Starship { 133 | """The ID of the starship""" 134 | id: ID! 135 | """The name of the starship""" 136 | name: String! 137 | """Length of the starship, along the longest axis""" 138 | length(unit: LengthUnit = METER): Float 139 | coordinates: [[Float!]!] 140 | } 141 | union SearchResult = Human | Droid | Starship -------------------------------------------------------------------------------- /codegen/__tests__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/codegen/__tests__/.gitkeep -------------------------------------------------------------------------------- /codegen/bin/index: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/CLI.js') -------------------------------------------------------------------------------- /codegen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@timkendall/tql-gen", 3 | "version": "1.0.0-rc.10", 4 | "description": "Code generator for @timkendall/tql.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "bin" 10 | ], 11 | "bin": { 12 | "tql-gen": "bin/index" 13 | }, 14 | "scripts": { 15 | "build": "tsc -b", 16 | "build:release": "tsc --build tsconfig.release.json", 17 | "clean": "rm -rf dist tsconfig.tsbuildinfo tsconfig.release.tsbuildinfo", 18 | "dev": "tsc -b -w", 19 | "test": "jest --cache=false", 20 | "type-test": "tsd", 21 | "format": "prettier --write", 22 | "version": "auto-changelog -p && git add CHANGELOG.md" 23 | }, 24 | "author": "Tim Kendall", 25 | "license": "MIT", 26 | "dependencies": { 27 | "ast-types": "^0.14.2", 28 | "fs-extra": "^9.0.1", 29 | "graphql": "^16.3.0", 30 | "jssha": "^3.2.0", 31 | "node-fetch": "^2.6.1", 32 | "outvariant": "^1.2.1", 33 | "prettier": "^2.5.1", 34 | "ts-poet": "^4.5.0", 35 | "ts-toolbelt": "^9.6.0", 36 | "yargs": "^17.5.1" 37 | }, 38 | "devDependencies": { 39 | "@arkweid/lefthook": "^0.7.7", 40 | "@types/fs-extra": "^9.0.2", 41 | "@types/jest": "^27.4.0", 42 | "@types/node": "^16.11.7", 43 | "@types/node-fetch": "^2.5.7", 44 | "@types/prettier": "^2.1.5", 45 | "auto-changelog": "^2.2.1", 46 | "jest": "^27.4.5", 47 | "ts-jest": "^27.1.2", 48 | "ts-node": "^10.4.0", 49 | "typescript": "^4.5.4" 50 | }, 51 | "publishConfig": { 52 | "access": "public" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /codegen/src/CLI.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import { buildClientSchema, getIntrospectionQuery, printSchema } from "graphql"; 3 | import fetch from "node-fetch"; 4 | import path from "path"; 5 | import Yargs from "yargs/yargs"; 6 | 7 | import { render } from "./render"; 8 | 9 | run().catch((e) => { 10 | console.error(e.message); 11 | process.exit(1); 12 | }); 13 | 14 | async function run() { 15 | const argv = Yargs(process.argv.slice(2)) 16 | .usage( 17 | "$0 ", 18 | "Generate a fluent TypeScript client for your GraphQL API.", 19 | ) 20 | .positional("schema", { 21 | type: "string", 22 | describe: "ex. https://graphql.org/swapi-graphql/", 23 | }) 24 | .options({ 25 | headers: { type: "array" }, 26 | output: { type: "string", describe: "Path of output file (typescript)" }, 27 | }) 28 | .alias("o", "output") 29 | .demandOption(["schema"]) 30 | .help("help").argv; 31 | 32 | const schemaPath = argv.schema; 33 | 34 | const schema = schemaPath.startsWith("http") 35 | ? await remoteSchema(schemaPath, { 36 | headers: normalizeHeaders(argv.headers), 37 | }) 38 | : await localSchema(schemaPath); 39 | 40 | const renderedSchema = render(schema); 41 | 42 | if (argv.output) { 43 | const outputPath = path.resolve(argv.output); 44 | console.log("Writing to: ", outputPath); 45 | fs.writeFile(outputPath, renderedSchema); 46 | } else { 47 | process.stdout.write(renderedSchema); 48 | } 49 | } 50 | 51 | function normalizeHeaders(headers: any): Record { 52 | if (typeof headers === "string") { 53 | return normalizeHeaders([headers]); 54 | } 55 | if (Array.isArray(headers)) { 56 | const entries = headers 57 | .map(headerArg => { 58 | if (typeof headerArg !== "string") { 59 | console.warn(`Invalid header ignored: ${headerArg}`); 60 | return null; 61 | } 62 | const parts = headerArg.split(":"); 63 | if (parts.length !== 2) { 64 | console.warn(`Invalid header ignored: ${headerArg}`); 65 | return null; 66 | } 67 | return parts.map(it => it.trim()); 68 | }) 69 | .filter(Boolean) as [string, string][]; 70 | return Object.fromEntries(entries); 71 | } 72 | if (typeof headers === "object") { 73 | const entries = Object 74 | .entries(headers) 75 | .map(([key, value]) => { 76 | if (typeof value !== "string") { 77 | console.warn(`Invalid header ignored: ${key}`); 78 | return null; 79 | } 80 | return [key, value]; 81 | }) 82 | .filter(Boolean) as [string, string][]; 83 | return Object.fromEntries(entries); 84 | } 85 | return {}; 86 | } 87 | 88 | async function localSchema(path: string) { 89 | const typeDefs = await fs.readFile(path, "utf-8"); 90 | return typeDefs; 91 | } 92 | 93 | async function remoteSchema(url: string, options: { 94 | headers: Record; 95 | }) { 96 | const { data, errors } = await fetch(url, { 97 | method: "post", 98 | headers: { 99 | "Content-Type": "application/json", 100 | ...options.headers, 101 | }, 102 | body: JSON.stringify({ 103 | operationName: "IntrospectionQuery", 104 | query: getIntrospectionQuery(), 105 | }), 106 | }).then((res) => res.json()); 107 | 108 | if (errors) { 109 | throw new Error("Error fetching remote schema!"); 110 | } 111 | 112 | return printSchema(buildClientSchema(data)); 113 | } 114 | -------------------------------------------------------------------------------- /codegen/src/__tests__/render.test.ts: -------------------------------------------------------------------------------- 1 | import { render } from "../render"; 2 | 3 | describe.skip("Codegen", () => { 4 | describe("schema", () => { 5 | describe("scalars", () => { 6 | it("converts ScalarTypes to primitives", () => { 7 | const input = ` 8 | scalar String 9 | scalar Int 10 | scalar Float 11 | scalar ID 12 | scalar Boolean 13 | `; 14 | 15 | const output = render(input); 16 | 17 | expect(output).toBe( 18 | expect.stringContaining(` 19 | interface ISchema { 20 | String: string 21 | Int: number 22 | Float: number 23 | ID: string 24 | Boolean: boolean 25 | } 26 | `) 27 | ); 28 | }); 29 | 30 | it("converts custom scalars to string types", () => { 31 | const input = ` 32 | scalar DateTime 33 | `; 34 | 35 | const output = render(input); 36 | 37 | expect(output).toBe( 38 | expect.stringContaining(` 39 | interface ISchema { 40 | String: string 41 | Int: number 42 | Float: number 43 | ID: string 44 | Boolean: boolean 45 | DateTime: string 46 | } 47 | `) 48 | ); 49 | }); 50 | }); 51 | 52 | describe("enums", () => { 53 | it("converts EnumTypes to enums", () => { 54 | const input = ` 55 | enum Foo { 56 | BAR 57 | BAZ 58 | } 59 | `; 60 | 61 | const output = render(input); 62 | 63 | expect(output).toBe( 64 | expect.stringContaining(` 65 | interface ISchema { 66 | Foo: Foo 67 | } 68 | 69 | export enum Foo { 70 | BAR = 'BAR', 71 | BAZ = 'BAZ' 72 | } 73 | `) 74 | ); 75 | }); 76 | }); 77 | 78 | describe("objects", () => { 79 | it.todo("converts ObjectTypes to interfaces"); 80 | }); 81 | 82 | describe("input objects", () => { 83 | it.todo("converts InputObjectTypes to interfaces"); 84 | }); 85 | 86 | describe("interfaces", () => { 87 | it.todo("converts InterfaceTypes to interfaces"); 88 | }); 89 | 90 | describe("unions", () => { 91 | it.todo("converts UnionTypes to unions"); 92 | }); 93 | }); 94 | 95 | describe("selectors", () => { 96 | describe("scalars", () => { 97 | it.todo("generates a method for defining a `Field` selection"); 98 | }); 99 | 100 | describe("enums", () => { 101 | it.todo("generates a method for defining a `Field` selection"); 102 | }); 103 | 104 | describe("objects", () => { 105 | it.todo("generates a method for defining a `Field` selection"); 106 | }); 107 | 108 | describe("interfaces", () => { 109 | it.todo( 110 | "generates an `on` method for defining an `InlineFragment` selection" 111 | ); 112 | }); 113 | 114 | describe("unions", () => { 115 | it.todo( 116 | "generates an `on` method for defining an `InlineFragment` selection" 117 | ); 118 | }); 119 | }); 120 | 121 | describe("top-level API", () => { 122 | // for defining variables 123 | it.todo("exposes a `$` fn"); 124 | // for constructing inline-fragments 125 | it.todo("exposes a `on` selector fn"); 126 | }); 127 | 128 | describe("utilities", () => { 129 | it.todo("generates a const `_ENUM_VALUES` object"); 130 | it.todo("generates a const `SCHEMA_SHA` string"); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /codegen/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./transforms"; 2 | export * from "./utils"; 3 | export * from "./render"; 4 | -------------------------------------------------------------------------------- /codegen/src/printers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputType, 3 | GraphQLScalarType, 4 | GraphQLEnumType, 5 | GraphQLInputObjectType, 6 | } from "graphql"; 7 | 8 | import { inputType, listType, toPrimitive } from "./utils"; 9 | 10 | export const printInputType = (type: GraphQLInputType): string => { 11 | const isList = listType(type); 12 | const base = inputType(type); 13 | 14 | return ( 15 | (() => { 16 | if (base instanceof GraphQLScalarType) { 17 | return toPrimitive(base); 18 | } else if (base instanceof GraphQLEnumType) { 19 | return base.name; 20 | } else if (base instanceof GraphQLInputObjectType) { 21 | return "I" + base.name; 22 | } else { 23 | throw new Error("Unable to render inputType."); 24 | } 25 | })() + (isList ? "[]" : "") 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /codegen/src/render.ts: -------------------------------------------------------------------------------- 1 | import { parse, buildSchema, visit, GraphQLEnumType, Kind, OperationTypeNode } from "graphql"; 2 | import type { Code } from "ts-poet"; 3 | import prettier from "prettier"; 4 | 5 | import { typeTransform, selectorInterfaceTransform } from "./transforms"; 6 | import { printType } from "./utils"; 7 | 8 | interface ASTNode { 9 | kind: string 10 | } 11 | 12 | export const render = (sdl: string): string => { 13 | const ast = parse(sdl, { noLocation: true }); 14 | const schema = buildSchema(sdl); 15 | 16 | const transforms = [ 17 | typeTransform(ast, schema), 18 | selectorInterfaceTransform(ast, schema), 19 | ]; 20 | 21 | // additive transforms 22 | const results = transforms.map( 23 | (vistor) => visit(ast, vistor) 24 | ) as unknown as Array<{ readonly definitions: Code[] }> ; 25 | 26 | const types = Object.values(schema.getTypeMap()).filter( 27 | (type) => !type.name.startsWith("__") 28 | ); 29 | 30 | const enumValues = new Set( 31 | Object.values(schema.getTypeMap()) 32 | .filter((type) => type instanceof GraphQLEnumType) 33 | .flatMap((type) => 34 | (type as GraphQLEnumType).getValues().map((value) => value.value) 35 | ) 36 | ); 37 | 38 | const ENUMS = ` 39 | Object.freeze({ 40 | ${Array.from(enumValues) 41 | .map((value) => `${value}: true`) 42 | .join(",\n")} 43 | } as const) 44 | `; 45 | 46 | const typeMap = ` 47 | export interface ISchema { 48 | ${types.map(printType).join("\n")} 49 | } 50 | `; 51 | 52 | const source = 53 | ` 54 | import { buildASTSchema, Kind, OperationTypeNode } from 'graphql' 55 | 56 | import { 57 | TypeConditionError, 58 | NamedType, 59 | Field, 60 | InlineFragment, 61 | Argument, 62 | Variable, 63 | Selection, 64 | SelectionSet, 65 | SelectionBuilder, 66 | namedType, 67 | field, 68 | inlineFragment, 69 | argument, 70 | selectionSet 71 | } from '@timkendall/tql' 72 | 73 | export type { Result, SelectionResult, Variables } from '@timkendall/tql' 74 | export { $ } from '@timkendall/tql' 75 | 76 | ` + 77 | ` 78 | export const SCHEMA = buildASTSchema(${stringifyAST(ast)}) 79 | 80 | export const ENUMS = ${ENUMS} 81 | 82 | ${typeMap} 83 | ` + 84 | results 85 | .flatMap((result) => 86 | result.definitions.map((code) => code.toCodeString()) 87 | ) 88 | .join("\n"); 89 | 90 | return prettier.format(source, { parser: "typescript" }); 91 | }; 92 | 93 | const stringifyAST = (ast: ASTNode) => { 94 | const acc: string[] = []; 95 | accumulateASTNode(ast, acc) 96 | return acc.join("\n") 97 | } 98 | 99 | const reverseRecord = >(input: TRecord) => Object.fromEntries(Object.entries(input).map(([k, v]) => [v, k])) 100 | 101 | const kindRevMapping = reverseRecord(Kind) 102 | const operationTypeRevMapping = reverseRecord(OperationTypeNode) 103 | 104 | const accumulateASTNode = ( 105 | astNode: ASTNode, 106 | acc: string[] 107 | ) => { 108 | acc.push('{') 109 | for (const [k,v] of Object.entries(astNode)) { 110 | if (v === undefined) continue 111 | acc.push(`${JSON.stringify(k)}: `) 112 | if (Array.isArray(v)) { 113 | acc.push(`[`) 114 | for (const childNode of v) { 115 | accumulateASTNode(childNode, acc) 116 | acc.push(',') 117 | } 118 | acc.push(']') 119 | } else if (typeof v === "object" && typeof v.kind === "string") { 120 | accumulateASTNode(v, acc) 121 | } else if (k === "kind" && kindRevMapping[v]) { 122 | acc.push(`Kind.${kindRevMapping[v]}`) 123 | } else if (k === "operation" && operationTypeRevMapping[v]) { 124 | acc.push(`OperationTypeNode.${operationTypeRevMapping[v]}`) 125 | } else { 126 | acc.push(JSON.stringify(v)) 127 | } 128 | acc.push(',') 129 | } 130 | acc.push('}') 131 | } 132 | 133 | -------------------------------------------------------------------------------- /codegen/src/transforms/index.ts: -------------------------------------------------------------------------------- 1 | export { transform as typeTransform } from "./types"; 2 | export { transform as selectorInterfaceTransform } from "./selectors"; 3 | -------------------------------------------------------------------------------- /codegen/src/transforms/selectors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | ASTVisitor, 4 | Kind, 5 | GraphQLArgument, 6 | GraphQLField, 7 | GraphQLObjectType, 8 | GraphQLNonNull, 9 | GraphQLScalarType, 10 | GraphQLEnumType, 11 | GraphQLUnionType, 12 | GraphQLInterfaceType, 13 | DocumentNode, 14 | } from "graphql"; 15 | import { imp, code } from "ts-poet"; 16 | import { invariant } from "outvariant"; 17 | 18 | import { printInputType } from "../printers"; 19 | import { inputType, outputType, toPrimitive, toLower } from "../utils"; 20 | 21 | const printConditionalNamedType = (types: string[]) => { 22 | const [first, ...rest] = types; 23 | 24 | if (rest.length === 0) { 25 | return `I${first}`; 26 | } else { 27 | return types 28 | .map((t) => `F extends "${t}" ? I${t} : `) 29 | .join("") 30 | .concat(" never"); 31 | } 32 | }; 33 | 34 | // ex. F extends "Human" ? HumanSelector : DroidSelector 35 | const printConditionalSelectorArg = (types: string[]) => { 36 | const [first, ...rest] = types; 37 | 38 | if (rest.length === 0) { 39 | return `I${first}Selector`; 40 | } else { 41 | return types 42 | .map((t) => `F extends "${t}" ? I${t}Selector : `) 43 | .join("") 44 | .concat(" never"); 45 | } 46 | }; 47 | 48 | const printArgument = (arg: GraphQLArgument): string => { 49 | const type = inputType(arg.type); 50 | const typename = 51 | type instanceof GraphQLScalarType 52 | ? toPrimitive(type) 53 | : type instanceof GraphQLEnumType 54 | ? type.toString() 55 | : "I" + type.toString(); 56 | 57 | return `Argument<"${arg.name}", V['${arg.name}']>`; 58 | }; 59 | 60 | const printVariable = (arg: GraphQLArgument): string => { 61 | return `${arg.name}${ 62 | arg.type instanceof GraphQLNonNull ? "" : "?" 63 | }: Variable | ${printInputType(arg.type)}`; 64 | }; 65 | 66 | const printMethod = (field: GraphQLField): string => { 67 | const { name, args } = field; 68 | 69 | const type = outputType(field.type); 70 | 71 | const comments = [ 72 | field.description && `@description ${field.description}`, 73 | field.deprecationReason && `@deprecated ${field.deprecationReason}`, 74 | ].filter(Boolean); 75 | 76 | const jsDocComment = 77 | comments.length > 0 78 | ? ` 79 | /** 80 | ${comments.map((comment) => "* " + comment).join("\n")} 81 | */ 82 | ` 83 | : ""; 84 | 85 | if (type instanceof GraphQLScalarType || type instanceof GraphQLEnumType) { 86 | // @todo render arguments correctly 87 | return args.length > 0 88 | ? jsDocComment.concat( 89 | `${name}: (variables) => field("${name}", Object.entries(variables).map(([k, v]) => argument(k, v)) as any),` 90 | ) 91 | : jsDocComment.concat(`${name}: () => field("${name}"),`); 92 | } else { 93 | const renderArgument = (arg: GraphQLArgument): string => { 94 | return `argument("${arg.name}", variables.${arg.name})`; 95 | }; 96 | 97 | // @todo restrict allowed Field types 98 | return args.length > 0 99 | ? ` 100 | ${jsDocComment} 101 | ${name}:( 102 | variables, 103 | select, 104 | ) => field("${name}", Object.entries(variables).map(([k, v]) => argument(k, v)) as any, selectionSet(select(${type.toString()}Selector))), 105 | ` 106 | : ` 107 | ${jsDocComment} 108 | ${name}: ( 109 | select, 110 | ) => field("${name}", undefined as never, selectionSet(select(${type.toString()}Selector))), 111 | `; 112 | } 113 | }; 114 | 115 | const printSignature = (field: GraphQLField): string => { 116 | const { name, args } = field; 117 | 118 | const type = outputType(field.type); 119 | 120 | const comments = [ 121 | field.description && `@description ${field.description}`, 122 | field.deprecationReason && `@deprecated ${field.deprecationReason}`, 123 | ].filter(Boolean) as string[]; 124 | 125 | const jsDocComment = 126 | comments.length > 0 127 | ? ` 128 | /** 129 | ${comments.map((comment) => "* " + comment).join("\n")} 130 | */ 131 | ` 132 | : ""; 133 | 134 | // @todo define Args type parameter as mapped type OR non-constant (i.e Array | Argument<...>>) 135 | if (type instanceof GraphQLScalarType || type instanceof GraphQLEnumType) { 136 | return args.length > 0 137 | ? `${jsDocComment}\n readonly ${name}: (variables: V) => Field<"${name}", [ ${args 140 | .map(printArgument) 141 | .join(", ")} ]>` 142 | : `${jsDocComment}\n readonly ${name}: () => Field<"${name}">`; 143 | } else { 144 | // @todo restrict allowed Field types 145 | return args.length > 0 146 | ? ` 147 | ${jsDocComment} 148 | readonly ${name}: >( 151 | variables: V, 152 | select: (t: I${type.toString()}Selector) => T 153 | ) => Field<"${name}", [ ${args 154 | .map(printArgument) 155 | .join(", ")} ], SelectionSet>, 156 | ` 157 | : ` 158 | ${jsDocComment} 159 | readonly ${name}: >( 160 | select: (t: I${type.toString()}Selector) => T 161 | ) => Field<"${name}", never, SelectionSet>, 162 | `; 163 | } 164 | }; 165 | 166 | export const transform = ( 167 | ast: DocumentNode, 168 | schema: GraphQLSchema 169 | ): ASTVisitor => { 170 | // const Field = imp("Field@timkendall@tql"); 171 | // const Argument = imp("Argument@timkendall@tql"); 172 | // const Variable = imp("Variable@timkendall@tql"); 173 | // const InlineFragment = imp("InlineFragment@timkendall@tql"); 174 | 175 | return { 176 | [Kind.DIRECTIVE_DEFINITION]: () => null, 177 | 178 | [Kind.SCALAR_TYPE_DEFINITION]: () => null, 179 | 180 | [Kind.ENUM_TYPE_DEFINITION]: (node) => { 181 | return null; 182 | }, 183 | 184 | [Kind.ENUM_VALUE_DEFINITION]: (node) => { 185 | return null; 186 | }, 187 | 188 | [Kind.INPUT_OBJECT_TYPE_DEFINITION]: (def) => { 189 | return null; 190 | }, 191 | 192 | [Kind.OBJECT_TYPE_DEFINITION]: (node) => { 193 | const typename = node.name.value; 194 | const type = schema.getType(typename); 195 | 196 | invariant( 197 | type instanceof GraphQLObjectType, 198 | `Type "${typename}" was not instance of expected class GraphQLObjectType.` 199 | ); 200 | 201 | const fields = Object.values(type.getFields()); 202 | 203 | return code` 204 | ${/* selector interface */ ""} 205 | interface I${type.name}Selector { 206 | readonly __typename: () => Field<"__typename"> 207 | ${fields.map(printSignature).join("\n")} 208 | } 209 | 210 | ${/* selector object */ ""} 211 | const ${typename}Selector: I${typename}Selector = { 212 | __typename: () => field("__typename"), 213 | ${fields.map(printMethod).join("\n")} 214 | } 215 | 216 | ${/* select fn */ ""} 217 | export const ${toLower( 218 | typename 219 | )} = >(select: (t: I${typename}Selector) => T) => new SelectionBuilder(SCHEMA as any, "${typename}", select(${typename}Selector)) 220 | `; 221 | }, 222 | 223 | [Kind.INTERFACE_TYPE_DEFINITION]: (node) => { 224 | const typename = node.name.value; 225 | const type = schema.getType(typename); 226 | 227 | invariant( 228 | type instanceof GraphQLInterfaceType, 229 | `Type "${typename}" was not instance of expected class GraphQLInterfaceType.` 230 | ); 231 | 232 | // @note Get all implementors of this union 233 | const implementations = schema 234 | .getPossibleTypes(type) 235 | .map((type) => type.name); 236 | 237 | const fields = Object.values(type.getFields()); 238 | 239 | return code` 240 | ${/* selector interface */ ""} 241 | interface I${type.name}Selector { 242 | readonly __typename: () => Field<"__typename"> 243 | 244 | ${fields.map(printSignature).join("\n")} 245 | 246 | readonly on: , F extends ${implementations 247 | .map((name) => `"${name}"`) 248 | .join(" | ")}>( 249 | type: F, 250 | select: (t: ${printConditionalSelectorArg( 251 | implementations.map((name) => name) 252 | )}) => T 253 | ) => InlineFragment, SelectionSet> 254 | } 255 | 256 | ${/* selector object */ ""} 257 | const ${typename}Selector: I${typename}Selector = { 258 | __typename: () => field("__typename"), 259 | 260 | ${fields.map(printMethod).join("\n")} 261 | 262 | on: ( 263 | type, 264 | select, 265 | ) => { 266 | switch(type) { 267 | ${implementations 268 | .map( 269 | (name) => ` 270 | case "${name}": { 271 | return inlineFragment( 272 | namedType("${name}"), 273 | selectionSet(select(${name}Selector as Parameters[0])), 274 | ) 275 | } 276 | ` 277 | ) 278 | .join("\n")} 279 | default: 280 | throw new TypeConditionError({ 281 | selectedType: type, 282 | abstractType: "${type.name}", 283 | }) 284 | } 285 | }, 286 | } 287 | 288 | ${/* select fn */ ""} 289 | export const ${toLower( 290 | typename 291 | )} = >(select: (t: I${typename}Selector) => T) => new SelectionBuilder(SCHEMA as any, "${typename}", select(${typename}Selector)) 292 | `; 293 | }, 294 | 295 | [Kind.UNION_TYPE_DEFINITION]: (node) => { 296 | const typename = node.name.value; 297 | const type = schema.getType(typename); 298 | 299 | invariant( 300 | type instanceof GraphQLUnionType, 301 | `Type "${typename}" was not instance of expected class GraphQLUnionType.` 302 | ); 303 | 304 | // @note Get all implementors of this union 305 | const implementations = schema 306 | .getPossibleTypes(type) 307 | .map((type) => type.name); 308 | 309 | return code` 310 | ${/* selector interface */ ""} 311 | interface I${type.name}Selector { 312 | readonly __typename: () => Field<"__typename"> 313 | 314 | readonly on: , F extends ${implementations 315 | .map((name) => `"${name}"`) 316 | .join(" | ")}>( 317 | type: F, 318 | select: (t: ${printConditionalSelectorArg( 319 | implementations.map((name) => name) 320 | )}) => T 321 | ) => InlineFragment, SelectionSet> 322 | } 323 | 324 | ${/* selector object */ ""} 325 | const ${typename}Selector: I${typename}Selector = { 326 | __typename: () => field("__typename"), 327 | 328 | on: ( 329 | type, 330 | select, 331 | ) => { 332 | switch(type) { 333 | ${implementations 334 | .map( 335 | (name) => ` 336 | case "${name}": { 337 | return inlineFragment( 338 | namedType("${name}"), 339 | selectionSet(select(${name}Selector as Parameters[0])), 340 | ) 341 | } 342 | ` 343 | ) 344 | .join("\n")} 345 | default: 346 | throw new TypeConditionError({ 347 | selectedType: type, 348 | abstractType: "${type.name}", 349 | }) 350 | } 351 | }, 352 | } 353 | 354 | ${/* select fn */ ""} 355 | export const ${toLower( 356 | typename 357 | )} = >(select: (t: I${typename}Selector) => T) => new SelectionBuilder(SCHEMA as any, "${typename}", select(${typename}Selector)) 358 | `; 359 | }, 360 | 361 | [Kind.SCHEMA_DEFINITION]: (node) => { 362 | return null; 363 | }, 364 | }; 365 | }; 366 | -------------------------------------------------------------------------------- /codegen/src/transforms/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | ASTVisitor, 4 | Kind, 5 | GraphQLArgument, 6 | isNonNullType, 7 | GraphQLField, 8 | GraphQLObjectType, 9 | GraphQLInputObjectType, 10 | GraphQLInputType, 11 | GraphQLNonNull, 12 | GraphQLScalarType, 13 | GraphQLEnumType, 14 | GraphQLUnionType, 15 | GraphQLInterfaceType, 16 | DocumentNode, 17 | GraphQLInputField, 18 | } from "graphql"; 19 | import { code } from "ts-poet"; 20 | import { invariant } from "outvariant"; 21 | 22 | import { printInputType } from "../printers"; 23 | import { inputType, outputType, listType, toPrimitive } from "../utils"; 24 | 25 | const printVariable = (arg: GraphQLArgument): string => { 26 | return `${arg.name}: ${printInputType(arg.type)} ${ 27 | arg.type instanceof GraphQLNonNull ? "" : "| undefined" 28 | }`; 29 | }; 30 | 31 | const printField = (field: GraphQLField): string => { 32 | const { args } = field; 33 | 34 | const isList = listType(field.type); 35 | const isNonNull = field.type instanceof GraphQLNonNull; 36 | const type = outputType(field.type); 37 | 38 | const printVariables = () => { 39 | return args.length > 0 40 | ? `(variables: { ${args.map(printVariable).join(", ")} })` 41 | : ""; 42 | }; 43 | 44 | if (type instanceof GraphQLScalarType) { 45 | return ( 46 | `${args.length > 0 ? "" : "readonly"} ${field.name}${printVariables()}: ${ 47 | isList ? `ReadonlyArray<${toPrimitive(type)}>` : `${toPrimitive(type)}` 48 | }` + (isNonNull ? "" : " | null") 49 | ); 50 | } else if (type instanceof GraphQLEnumType) { 51 | return ( 52 | `${args.length > 0 ? "" : "readonly"} ${field.name}${printVariables()}: ${ 53 | isList ? `ReadonlyArray<${type.name}>` : `${type.name}` 54 | }` + (isNonNull ? "" : " | null") 55 | ); 56 | } else if ( 57 | type instanceof GraphQLInterfaceType || 58 | type instanceof GraphQLUnionType || 59 | type instanceof GraphQLObjectType 60 | ) { 61 | return ( 62 | `${args.length > 0 ? "" : "readonly"} ${field.name}${printVariables()}: ${ 63 | isList ? `ReadonlyArray` : `I${type.name}` 64 | }` + (isNonNull ? "" : " | null") 65 | ); 66 | } else { 67 | throw new Error("Unable to print field."); 68 | } 69 | }; 70 | 71 | export const transform = ( 72 | ast: DocumentNode, 73 | schema: GraphQLSchema 74 | ): ASTVisitor => { 75 | // @note needed to serialize inline enum values correctly at runtime 76 | const enumValues = new Set(); 77 | 78 | return { 79 | [Kind.DIRECTIVE_DEFINITION]: () => null, 80 | 81 | [Kind.SCALAR_TYPE_DEFINITION]: () => null, 82 | 83 | [Kind.ENUM_TYPE_DEFINITION]: (node) => { 84 | const typename = node.name.value; 85 | const values = node.values?.map((v) => v.name.value) ?? []; 86 | 87 | const printMember = (member: string): string => { 88 | return `${member} = "${member}"`; 89 | }; 90 | 91 | return code` 92 | export enum ${typename} { 93 | ${values.map(printMember).join(",\n")} 94 | } 95 | `; 96 | }, 97 | 98 | [Kind.ENUM_VALUE_DEFINITION]: (node) => { 99 | enumValues.add(node.name.value); 100 | return null; 101 | }, 102 | 103 | [Kind.INPUT_OBJECT_TYPE_DEFINITION]: (node) => { 104 | const typename = node.name.value; 105 | const type = schema.getType(typename); 106 | 107 | invariant( 108 | type instanceof GraphQLInputObjectType, 109 | `Type "${typename}" was not instance of expected class GraphQLInputObjectType.` 110 | ); 111 | 112 | const fields = Object.values(type.getFields()); 113 | 114 | const printField = (field: GraphQLInputField) => { 115 | const isList = listType(field.type); 116 | const isNonNull = isNonNullType(field.type); 117 | const baseType = inputType(field.type); 118 | 119 | let tsType: string; 120 | 121 | if (baseType instanceof GraphQLScalarType) { 122 | tsType = toPrimitive(baseType); 123 | } else if (baseType instanceof GraphQLEnumType) { 124 | tsType = baseType.name; 125 | } else if (baseType instanceof GraphQLInputObjectType) { 126 | tsType = "I" + baseType.name; 127 | } else { 128 | throw new Error("Unable to render inputField!"); 129 | } 130 | 131 | return [ 132 | field.name, 133 | isNonNull ? ":" : "?:", 134 | " ", 135 | tsType, 136 | isList ? "[]" : "", 137 | ].join(""); 138 | }; 139 | 140 | return code` 141 | export interface I${typename} { 142 | ${fields.map(printField).join("\n")} 143 | } 144 | `; 145 | }, 146 | 147 | [Kind.OBJECT_TYPE_DEFINITION]: (node) => { 148 | const typename = node.name.value; 149 | const type = schema.getType(typename); 150 | 151 | invariant( 152 | type instanceof GraphQLObjectType, 153 | `Type "${typename}" was not instance of expected class GraphQLObjectType.` 154 | ); 155 | 156 | const fields = Object.values(type.getFields()); 157 | const interfaces = type.getInterfaces(); 158 | 159 | // @note TypeScript only requires new fields to be defined on interface extendors 160 | const interfaceFields = interfaces.flatMap((i) => 161 | Object.values(i.getFields()).map((field) => field.name) 162 | ); 163 | const uncommonFields = fields.filter( 164 | (field) => !interfaceFields.includes(field.name) 165 | ); 166 | 167 | // @todo extend any implemented interfaces 168 | // @todo only render fields unique to this type 169 | const extensions = 170 | interfaces.length > 0 171 | ? `extends ${interfaces.map((i) => "I" + i.name).join(", ")}` 172 | : ""; 173 | 174 | return code` 175 | export interface I${typename} ${extensions} { 176 | readonly __typename: ${`"${typename}"`} 177 | ${uncommonFields.map(printField).join("\n")} 178 | } 179 | `; 180 | }, 181 | 182 | [Kind.INTERFACE_TYPE_DEFINITION]: (node) => { 183 | const typename = node.name.value; 184 | const type = schema.getType(typename); 185 | 186 | invariant( 187 | type instanceof GraphQLInterfaceType, 188 | `Type "${typename}" was not instance of expected class GraphQLInterfaceType.` 189 | ); 190 | 191 | // @note Get all implementors of this union 192 | const implementations = schema 193 | .getPossibleTypes(type) 194 | .map((type) => type.name); 195 | 196 | const fields = Object.values(type.getFields()); 197 | 198 | return code` 199 | export interface I${typename} { 200 | readonly __typename: ${implementations 201 | .map((type) => `"${type}"`) 202 | .join(" | ")} 203 | ${fields.map(printField).join("\n")} 204 | } 205 | `; 206 | }, 207 | 208 | [Kind.UNION_TYPE_DEFINITION]: (node) => { 209 | const typename = node.name.value; 210 | const type = schema.getType(typename); 211 | 212 | invariant( 213 | type instanceof GraphQLUnionType, 214 | `Type "${typename}" was not instance of expected class GraphQLUnionType.` 215 | ); 216 | 217 | // @note Get all implementors of this union 218 | const implementations = schema 219 | .getPossibleTypes(type) 220 | .map((type) => type.name); 221 | 222 | return code` 223 | export type ${"I" + type.name} = ${implementations 224 | .map((type) => `I${type}`) 225 | .join(" | ")} 226 | `; 227 | }, 228 | 229 | [Kind.SCHEMA_DEFINITION]: (_) => { 230 | return null; 231 | }, 232 | }; 233 | }; 234 | -------------------------------------------------------------------------------- /codegen/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputType, 3 | GraphQLOutputType, 4 | GraphQLNonNull, 5 | GraphQLList, 6 | GraphQLScalarType, 7 | GraphQLNamedType, 8 | GraphQLEnumType, 9 | } from "graphql"; 10 | 11 | export function toUpper(word: string): string { 12 | return word.charAt(0).toUpperCase() + word.slice(1); 13 | } 14 | 15 | export function toLower(word: string): string { 16 | return word.charAt(0).toLowerCase() + word.slice(1); 17 | } 18 | 19 | export function inputType(type: GraphQLInputType): GraphQLInputType { 20 | if (type instanceof GraphQLNonNull) { 21 | return inputType(type.ofType); 22 | } else if (type instanceof GraphQLList) { 23 | return inputType(type.ofType); 24 | } else { 25 | return type; 26 | } 27 | } 28 | 29 | export function outputType(type: GraphQLOutputType): GraphQLOutputType { 30 | if (type instanceof GraphQLNonNull) { 31 | return outputType(type.ofType); 32 | } else if (type instanceof GraphQLList) { 33 | return outputType(type.ofType); 34 | } else { 35 | return type; 36 | } 37 | } 38 | 39 | export function listType(type: GraphQLOutputType | GraphQLInputType): boolean { 40 | if (type instanceof GraphQLNonNull) { 41 | return listType(type.ofType); 42 | } else if (type instanceof GraphQLList) { 43 | return true; 44 | } else { 45 | return false; 46 | } 47 | } 48 | 49 | export const toPrimitive = ( 50 | scalar: GraphQLScalarType 51 | ): "number" | "string" | "boolean" => { 52 | switch (scalar.name) { 53 | case "ID": 54 | case "String": 55 | return "string"; 56 | case "Boolean": 57 | return "boolean"; 58 | case "Int": 59 | case "Float": 60 | return "number"; 61 | default: 62 | return "string"; 63 | } 64 | }; 65 | 66 | export const printType = (type: GraphQLNamedType) => { 67 | if (type instanceof GraphQLScalarType) { 68 | return `${type.name}: ${toPrimitive(type)}`; 69 | } else if (type instanceof GraphQLEnumType) { 70 | return `${type.name}: ${type.name}`; 71 | } else { 72 | return `${type.name}: I${type.name}`; 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /codegen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "__tests__/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "noEmit": true, 6 | "rootDir": ".", 7 | "outDir": "dist", 8 | "sourceMap": true, 9 | "target": "ES2020", 10 | "module": "commonjs", 11 | "noUnusedLocals": false, 12 | "allowJs": false, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "moduleResolution": "node", 18 | "strictPropertyInitialization": false, 19 | "skipLibCheck": true, 20 | "incremental": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "lib": ["ES2020"] 23 | } 24 | } -------------------------------------------------------------------------------- /codegen/tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["src/**/*.test.ts"], 5 | "compilerOptions": { 6 | "rootDir": "src", 7 | "outDir": "dist", 8 | "noEmit": false, 9 | "sourceMap": true, 10 | } 11 | } -------------------------------------------------------------------------------- /docs/api/selection.md: -------------------------------------------------------------------------------- 1 | # `Selection` 2 | 3 | Todo 4 | 5 | ## `.toQuery(options: QueryOptions)` 6 | 7 | ## `.toSelectionSet` 8 | 9 | ## `.toFragment(name: string)` 10 | 11 | ## `.toInlineFragment` -------------------------------------------------------------------------------- /docs/client-examples/apollo.md: -------------------------------------------------------------------------------- 1 | # Apollo Client 2 | 3 | -------------------------------------------------------------------------------- /docs/client-examples/graphql-request.md: -------------------------------------------------------------------------------- 1 | # GraphQL Request 2 | 3 | -------------------------------------------------------------------------------- /docs/client-examples/urql.md: -------------------------------------------------------------------------------- 1 | # URQL 2 | -------------------------------------------------------------------------------- /docs/directives.md: -------------------------------------------------------------------------------- 1 | # Directives 2 | 3 | Coming Soon -------------------------------------------------------------------------------- /docs/fragments/inline.md: -------------------------------------------------------------------------------- 1 | # Inline Fragments 2 | 3 | - Use the provided `on(type: string)` selector on abstract (interface or union) types 4 | - Convert any selection to an inline fragment with `Selection.toInlineFragment` 5 | - Spread any selection into another ("virtual" fragments) 6 | 7 | ## Using the `on` selector 8 | 9 | ```typescript 10 | import { query } from './sdk' 11 | 12 | const QUERY = query(t => [ 13 | t.search({ text: 'hans' }, t => [ 14 | t.__typename(), 15 | 16 | t.on("Human", (t) => [ 17 | t.homePlanet() 18 | ]), 19 | ]) 20 | ]) 21 | ``` 22 | 23 | ## Converting a selection with `toInlineFragment` 24 | 25 | ```typescript 26 | import { character, query } from './sdk' 27 | 28 | const FRAGMENT = character(t => [ 29 | t.id(), 30 | ]).toInlineFragment() 31 | 32 | const QUERY = query(t => [ 33 | t.character({ id: '1001' }, t => [ 34 | FRAGMENT, 35 | ]) 36 | ]) 37 | ``` 38 | 39 | ## Spreading a selection 40 | 41 | 42 | ```typescript 43 | import { character, query } from './sdk' 44 | 45 | const CHARACTER_SELECTION = character(t => [ 46 | t.id(), 47 | ]) 48 | 49 | const QUERY = query(t => [ 50 | t.character({ id: '1001' }, t => [ 51 | ...CHARACTER_SELECTION, 52 | ]) 53 | ]) 54 | ``` -------------------------------------------------------------------------------- /docs/fragments/named.md: -------------------------------------------------------------------------------- 1 | # Named Fragments 2 | 3 | Coming Soon -------------------------------------------------------------------------------- /docs/operations.md: -------------------------------------------------------------------------------- 1 | # Operations 2 | 3 | ## Example 4 | 5 | ```typescript 6 | import { query } from './sdk' 7 | 8 | const QUERY = query(t => [ 9 | t.viewer(t => [ 10 | t.id() 11 | ]) 12 | ]).toOperation({ name: 'Viewer' }) 13 | ``` -------------------------------------------------------------------------------- /docs/quickstart/codegen.md: -------------------------------------------------------------------------------- 1 | # Codegen 2 | 3 | Codegen currently is required to be manually once at least once before usage. We provide the code generator as a seperate module to avoid unneccesary dependencies in the runtime module. 4 | 5 | ## Usage 6 | 7 | `npx @timkendall/tql-gen -o sdk.ts` 8 | 9 | - ``: HTTP(s) endpoint of a GraphQL API w/ introspection enabled or local `.graphql` schema file 10 | 11 | You can also pass headers (eg. for authentication) to your graphql API endpoint if needed: 12 | 13 | `npx @timkendall/tql-gen http://api.example.com/graphql --headers.Authorization="Bearer 20394823423"` 14 | 15 | or 16 | 17 | `npx @timkendall/tql-gen http://api.example.com/graphql --headers="Authorization: Bearer 20394823423"` 18 | 19 | In either usage patterns, you can pass multiple headers by repeated usage of `--headers` 20 | -------------------------------------------------------------------------------- /docs/quickstart/demo.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | Try tql out live against the [canonical SWAPI](https://github.com/graphql/swapi-graphql) on [CodeSandbox](https://codesandbox.io/s/tql-starwars-wlfg9?file=/src/index.ts&runonclick=1)! -------------------------------------------------------------------------------- /docs/quickstart/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | `npm install -S @timkendall/tql@beta` 4 | 5 | ## Deno Support 6 | 7 | Coming Soon! -------------------------------------------------------------------------------- /docs/variables.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 | Variables definitions can be created with the `$` helper. 4 | 5 | ```typescript 6 | import { query, $ } from './sdk' 7 | 8 | const QUERY = query(t => [ 9 | t.character({ id: $('characterId') }, t => [ 10 | t.id() 11 | ]) 12 | ]) 13 | ``` -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "incremental": true, 3 | "typescript": { 4 | "indentWidth": 2 5 | }, 6 | "json": { 7 | "indentWidth": 2 8 | }, 9 | "markdown": { 10 | }, 11 | "toml": { 12 | }, 13 | "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md,toml}"], 14 | "excludes": [ 15 | "**/node_modules", 16 | "**/*-lock.json" 17 | ], 18 | "plugins": [ 19 | "https://plugins.dprint.dev/typescript-0.60.0.wasm", 20 | "https://plugins.dprint.dev/json-0.14.0.wasm", 21 | "https://plugins.dprint.dev/markdown-0.12.0.wasm", 22 | "https://plugins.dprint.dev/toml-0.5.3.wasm" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/examples/.gitkeep -------------------------------------------------------------------------------- /examples/01-query.ts: -------------------------------------------------------------------------------- 1 | import type { SelectionSet } from '../src' 2 | 3 | import { query, ISchema, IQuery, Result } from './generated' 4 | 5 | const ExampleQuery = query(t => [ 6 | t.droid({ id: '001' }, t => [ 7 | t.__typename(), 8 | t.id(), 9 | t.name(), 10 | t.primaryFunction(), 11 | ]) 12 | ]) 13 | 14 | const result = {} as Result> 15 | 16 | result.droid?.id 17 | result.droid?.name 18 | result.droid?.primaryFunction -------------------------------------------------------------------------------- /examples/02-mutation.ts: -------------------------------------------------------------------------------- 1 | import type { SelectionSet } from '../src' 2 | 3 | import { mutation, Episode, $, ISchema, IMutation, Result, Variables } from './generated' 4 | 5 | const ExampleMutation = mutation(t => [ 6 | t.createReview({ 7 | episode: $('episode'), 8 | review: $('review') 9 | }, t => [ 10 | t.__typename(), 11 | t.commentary(), 12 | t.stars(), 13 | ]) 14 | ]) 15 | 16 | const variables: Variables> = { 17 | episode: Episode.EMPIRE, 18 | review: { 19 | stars: 1, 20 | commentary: 'Awesome!' 21 | } 22 | } 23 | const result = {} as Result> 24 | 25 | result.createReview?.__typename 26 | result.createReview?.commentary 27 | result.createReview?.stars -------------------------------------------------------------------------------- /examples/03-fragments.ts: -------------------------------------------------------------------------------- 1 | import type { SelectionSet } from '../src' 2 | 3 | import { query, LengthUnit, ISchema, IQuery, Result } from './generated' 4 | 5 | // @important TypeScript will be unable to infer our result type unless 6 | // the `` assertion is provided on the selection that includes fragments! 7 | const ExampleQuery = query(t => [ 8 | // example union selection 9 | t.search({ text: 'Foo'}, t => [ 10 | t.__typename(), 11 | 12 | t.on('Droid', t => [ 13 | t.primaryFunction(), 14 | ]), 15 | 16 | t.on('Human', t => [ 17 | t.homePlanet(), 18 | ]), 19 | 20 | t.on('Starship', t => [ 21 | t.length({ unit: LengthUnit.CUBIT }) 22 | ]) 23 | ]), 24 | 25 | // example interface selection 26 | t.character({ id: '001'}, t => [ 27 | t.__typename(), 28 | t.id(), 29 | 30 | t.friends(t => [ 31 | t.__typename(), 32 | 33 | t.on('Droid', t => [ 34 | t.primaryFunction() 35 | ]), 36 | 37 | t.on('Human', t => [ 38 | t.homePlanet(), 39 | ]), 40 | ]) 41 | ]) 42 | ]) 43 | 44 | const result = {} as Result> 45 | 46 | // infered properly as `"Droid" | "Human" | "Starship" | undefined` 47 | result.search?.[0].__typename 48 | 49 | // exhaustive branches are computed properly 50 | if (result.search?.[0].__typename === 'Droid') { 51 | // `__typename` is narrowed properly to `"Droid"` 52 | result.search[0].__typename 53 | result.search[0].primaryFunction 54 | } else if (result.search?.[0].__typename === 'Human') { 55 | // `__typename` is narrowed properly to `"Human"` 56 | result.search[0].__typename 57 | result.search[0].homePlanet 58 | } else { 59 | // `__typename` is narrowed properly to `"Starship" | undefined` 60 | result.search?.[0].__typename 61 | result.search?.[0].length 62 | } 63 | 64 | 65 | 66 | // infered properly as `"Droid" | "Human" | undefined` 67 | result.character?.__typename 68 | result.character?.id 69 | 70 | // exhaustive branches are computed properly 71 | if (result.character?.__typename === 'Droid') { 72 | // `__typename` is narrowed properly to `"Droid"` 73 | result.character.__typename 74 | } 75 | 76 | // nested inference works as well 77 | if (result.character?.friends?.[0].__typename === 'Droid') { 78 | result.character.friends[0].primaryFunction 79 | } else { 80 | result.character?.friends?.[0].homePlanet 81 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./src"; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const pack = require('./package'); 2 | 3 | module.exports = { 4 | preset: "ts-jest", 5 | name: pack.name, 6 | displayName: pack.name, 7 | testEnvironment: "node", 8 | moduleDirectories: ['node_modules', 'src', '__tests__'], 9 | testPathIgnorePatterns: ['build'], 10 | testRegex: "(/(__tests__|src)/.*(\\.|/)(test|spec))\\.(ts|tsx)$", 11 | moduleDirectories: ['node_modules', 'src', '__tests__'] 12 | } -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | formatter: 5 | files: git diff --name-only --staged 6 | glob: "*.{ts}" 7 | run: pnpm dprint fmt {files} && git add {files} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@timkendall/tql", 3 | "author": "Timothy Kendall", 4 | "license": "MIT", 5 | "version": "1.0.0-rc.8", 6 | "description": "Write GraphQL queries in TypeScript.", 7 | "sideEffects": false, 8 | "keywords": [ 9 | "graphql", 10 | "typescript", 11 | "query builder", 12 | "codegen" 13 | ], 14 | "main": "dist/index.js", 15 | "types": "dist/index.d.ts", 16 | "files": [ 17 | "dist" 18 | ], 19 | "workspaces": [ 20 | ".", 21 | "codegen" 22 | ], 23 | "scripts": { 24 | "build": "tsc -b", 25 | "build:release": "tsc --build tsconfig.release.json", 26 | "build:website": "cd ./website && npm run build", 27 | "clean": "rm -rf dist tsconfig.tsbuildinfo tsconfig.release.tsbuildinfo", 28 | "dev": "tsc -b -w", 29 | "test": "jest --cache=false", 30 | "type-test": "tsd", 31 | "format": "dprint fmt", 32 | "version": "auto-changelog -p && git add CHANGELOG.md" 33 | }, 34 | "dependencies": { 35 | "@graphql-typed-document-node/core": "^3.1.1", 36 | "ts-toolbelt": "^9.6.0" 37 | }, 38 | "devDependencies": { 39 | "@apollo/client": "^3.5.10", 40 | "@arkweid/lefthook": "^0.7.7", 41 | "@types/deep-freeze": "^0.1.2", 42 | "@types/fs-extra": "^9.0.13", 43 | "@types/jest": "^27.4.0", 44 | "@types/node": "^16.11.7", 45 | "@types/node-fetch": "^2.5.12", 46 | "@types/yargs": "^15.0.14", 47 | "auto-changelog": "^2.3.0", 48 | "deep-freeze": "^0.0.1", 49 | "dprint": "^0.19.2", 50 | "graphql": "^16.3.0", 51 | "graphql-request": "^4.2.0", 52 | "jest": "^27.4.5", 53 | "ts-jest": "^27.1.2", 54 | "ts-node": "^10.4.0", 55 | "tsd": "^0.19.1", 56 | "typescript": "^4.5.4" 57 | }, 58 | "peerDependencies": { 59 | "graphql": "^16.0.0" 60 | }, 61 | "publishConfig": { 62 | "access": "public" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'codegen' 3 | - 'website' -------------------------------------------------------------------------------- /src/AST.ts: -------------------------------------------------------------------------------- 1 | import { Kind, OperationTypeNode } from "graphql/language"; 2 | import type { 3 | TypeNode, 4 | NamedTypeNode, 5 | ListTypeNode, 6 | NonNullTypeNode, 7 | ValueNode, 8 | VariableNode, 9 | ArgumentNode, 10 | DirectiveNode, 11 | SelectionNode, 12 | VariableDefinitionNode, 13 | SelectionSetNode, 14 | OperationDefinitionNode, 15 | DefinitionNode, 16 | DocumentNode, 17 | FieldNode, 18 | InlineFragmentNode, 19 | FragmentDefinitionNode, 20 | FragmentSpreadNode, 21 | } from "graphql/language"; 22 | 23 | export type Primitive = 24 | | string 25 | | number 26 | | bigint 27 | | boolean 28 | | symbol 29 | | null 30 | | undefined; 31 | 32 | export interface NonNullType | ListType> 33 | extends NonNullTypeNode { 34 | type: Type; 35 | } 36 | 37 | export const nonNull = | ListType>( 38 | type: Type 39 | ): NonNullType => ({ 40 | kind: Kind.NON_NULL_TYPE, 41 | type, 42 | }); 43 | 44 | export interface ListType | NonNullType> 45 | extends ListTypeNode { 46 | type: Type; 47 | } 48 | 49 | export interface NamedType extends NamedTypeNode { 50 | name: { kind: Kind.NAME; value: Name }; 51 | } 52 | 53 | export const namedType = ( 54 | name: string 55 | ): NamedType => ({ 56 | kind: Kind.NAMED_TYPE, 57 | name: { kind: Kind.NAME, value: name as Name }, 58 | }); 59 | 60 | export type Type = NamedType | ListType | NonNullType; 61 | 62 | export interface Variable extends VariableNode { 63 | name: { kind: Kind.NAME; value: Name }; 64 | } 65 | 66 | export const variable = (name: Name): Variable => ({ 67 | kind: Kind.VARIABLE, 68 | name: { 69 | kind: Kind.NAME, 70 | value: name, 71 | }, 72 | }); 73 | 74 | // @todo define extensions on `ValueNode` 75 | export type Value = Variable | Primitive; 76 | 77 | export interface Argument 78 | extends ArgumentNode { 79 | readonly name: { kind: Kind.NAME; value: Name }; 80 | } 81 | 82 | export const argument = ( 83 | name: Name, 84 | value: Value 85 | ): Argument => ({ 86 | kind: Kind.ARGUMENT, 87 | name: { kind: Kind.NAME, value: name }, 88 | value: toValueNode(value), 89 | }); 90 | 91 | export interface VariableDefinition, T extends Type> 92 | extends VariableDefinitionNode {} 93 | 94 | export const variableDefinition = , T extends Type>( 95 | variable: V, 96 | type: T 97 | ): VariableDefinition => ({ 98 | kind: Kind.VARIABLE_DEFINITION, 99 | variable, 100 | type, // TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode; 101 | 102 | // @todo 103 | // defaultValue?: ValueNode; 104 | // readonly directives?: ReadonlyArray; 105 | }); 106 | 107 | export interface SelectionSet> 108 | extends SelectionSetNode { 109 | selections: T; 110 | } 111 | 112 | export const selectionSet = >( 113 | selections: T 114 | ): SelectionSet => ({ 115 | kind: Kind.SELECTION_SET, 116 | selections, 117 | }); 118 | 119 | export interface Field< 120 | Name extends string, 121 | Arguments extends Array> | undefined = undefined, 122 | SS extends SelectionSet | undefined = undefined, 123 | AliasName extends string | undefined = undefined, 124 | > extends FieldNode { 125 | name: { kind: Kind.NAME; value: Name }; 126 | arguments?: Arguments; 127 | selectionSet?: SS; 128 | alias?: AliasNode; 129 | 130 | as(alias: AliasName): Field 131 | } 132 | 133 | type AliasNode = 134 | AliasName extends string 135 | ? { kind: Kind.NAME, value: AliasName } 136 | : undefined 137 | 138 | 139 | export const field = < 140 | Name extends string, 141 | Arguments extends Array> | undefined = undefined, 142 | SS extends SelectionSet | undefined = undefined 143 | >( 144 | name: Name, 145 | args?: Arguments, 146 | selectionSet?: SS 147 | ): Field => ({ 148 | kind: Kind.FIELD, 149 | name: { kind: Kind.NAME, value: name }, 150 | directives: [], 151 | arguments: args, 152 | alias: undefined, 153 | selectionSet: selectionSet, 154 | as(alias: AliasName) { 155 | return { 156 | ...this, 157 | alias: { kind: Kind.NAME, value: alias } as AliasNode 158 | } 159 | } 160 | }); 161 | 162 | export interface InlineFragment< 163 | TypeCondition extends NamedType, 164 | SS extends SelectionSet> 165 | > extends InlineFragmentNode {} 166 | 167 | export const inlineFragment = < 168 | TypeCondition extends NamedType, 169 | SS extends SelectionSet> 170 | >( 171 | typeCondition: TypeCondition, 172 | selectionSet: SS 173 | ): InlineFragment => ({ 174 | kind: Kind.INLINE_FRAGMENT, 175 | typeCondition, 176 | directives: [ 177 | /* @todo*/ 178 | ], 179 | selectionSet, 180 | }); 181 | 182 | export interface FragmentDefinition< 183 | Name extends string, 184 | TypeCondition extends NamedType, 185 | SS extends SelectionSet> 186 | > extends FragmentDefinitionNode { 187 | readonly name: { kind: Kind.NAME; value: Name }; 188 | readonly typeCondition: TypeCondition; 189 | readonly selectionSet: SS; 190 | } 191 | 192 | export const fragmentDefinition = < 193 | Name extends string, 194 | TypeCondition extends NamedType, 195 | SS extends SelectionSet> 196 | >( 197 | name: Name, 198 | typeCondition: TypeCondition, 199 | selectionSet: SS 200 | ): FragmentDefinition => ({ 201 | kind: Kind.FRAGMENT_DEFINITION, 202 | name: { kind: Kind.NAME, value: name }, 203 | typeCondition, 204 | selectionSet, 205 | // directives @todo 206 | }); 207 | 208 | export interface FragmentSpread 209 | extends FragmentSpreadNode { 210 | readonly name: { kind: Kind.NAME; value: Name }; 211 | // readonly directives?: ReadonlyArray; 212 | } 213 | 214 | // SelectionNode 215 | export type Selection = 216 | | Field 217 | | InlineFragment 218 | | FragmentSpread; 219 | 220 | export type Fragment = InlineFragment; /*| NamedFragment */ 221 | 222 | type AnyOp = OperationTypeNode.QUERY | OperationTypeNode.MUTATION | OperationTypeNode.SUBSCRIPTION 223 | 224 | export interface Operation< 225 | Op extends AnyOp, 226 | Name extends string, 227 | VariableDefinitions extends Array> | never, 228 | SS extends SelectionSet 229 | > extends OperationDefinitionNode { 230 | operation: Op; 231 | name: { kind: Kind.NAME; value: Name }; 232 | variableDefinitions: VariableDefinitions; 233 | selectionSet: SS; 234 | } 235 | 236 | export const operation = < 237 | Op extends AnyOp, 238 | Name extends string | never, 239 | VariableDefinitions extends Array> | never, 240 | SS extends SelectionSet 241 | >( 242 | op: Op, 243 | name: Name, 244 | selectionSet: SS, 245 | variableDefinitions: VariableDefinitions 246 | ): Operation => ({ 247 | kind: Kind.OPERATION_DEFINITION, 248 | name: { kind: Kind.NAME, value: name }, 249 | operation: op, 250 | variableDefinitions, 251 | selectionSet, 252 | directives: [ 253 | /* @todo */ 254 | ], 255 | }); 256 | 257 | // DefinitionNode 258 | export type Definition = Operation; // | Fragment 259 | 260 | export interface Document> 261 | extends DocumentNode { 262 | definitions: T; 263 | } 264 | 265 | export const document = >( 266 | definitions: T 267 | ): DocumentNode => ({ 268 | kind: Kind.DOCUMENT, 269 | definitions, 270 | }); 271 | 272 | // @todo use the proper `astFromValue` 273 | export const toValueNode = (value: any, enums: any[] = []): ValueNode => { 274 | if (typeof value === "string") { 275 | if (enums.some((e) => Object.values(e).includes(value))) 276 | return { kind: Kind.ENUM, value: value }; 277 | return { kind: Kind.STRING, value: value }; 278 | } else if (Number.isInteger(value)) { 279 | return { kind: Kind.INT, value: value }; 280 | } else if (typeof value === "number") { 281 | return { kind: Kind.FLOAT, value: String(value) }; 282 | } else if (typeof value === "boolean") { 283 | return { kind: Kind.BOOLEAN, value: value }; 284 | } else if (value === null || value === undefined) { 285 | return { kind: Kind.NULL }; 286 | } else if (Array.isArray(value)) { 287 | return { 288 | kind: Kind.LIST, 289 | values: value.map((v) => toValueNode(v, enums)), 290 | }; 291 | } else if (typeof value === "object") { 292 | if (value.kind && value.kind === "Variable") { 293 | return value; 294 | } else { 295 | return { 296 | kind: Kind.OBJECT, 297 | fields: Object.entries(value) 298 | .filter(([_, value]) => value !== undefined) 299 | .map(([key, value]) => { 300 | const keyValNode = toValueNode(value, enums); 301 | return { 302 | kind: Kind.OBJECT_FIELD, 303 | name: { kind: Kind.NAME, value: key }, 304 | value: keyValNode, 305 | }; 306 | }), 307 | }; 308 | } 309 | } else { 310 | throw new Error(`Unknown value type: ${value}`); 311 | } 312 | }; 313 | 314 | export function getBaseTypeNode(type: TypeNode): NamedTypeNode { 315 | if (type.kind === Kind.NON_NULL_TYPE) { 316 | return getBaseTypeNode(type.type); 317 | } else if (type.kind === Kind.LIST_TYPE) { 318 | return getBaseTypeNode(type.type); 319 | } else { 320 | return type; 321 | } 322 | } 323 | 324 | export function getBaseType(type: TypeNode): string { 325 | return getBaseTypeNode(type).name.value; 326 | } 327 | -------------------------------------------------------------------------------- /src/Result.ts: -------------------------------------------------------------------------------- 1 | import { TypedDocumentNode } from "@graphql-typed-document-node/core"; 2 | import { L, Test } from "ts-toolbelt"; 3 | 4 | import type { Field, InlineFragment, NamedType, Selection, SelectionSet } from "./AST"; 5 | 6 | // @note `Result` takes a root `Type` (TS) and `SelectionSet` (GQL) and recursively walks the 7 | // array of `Selection` nodes's (i.e `Field`, `InlineFragment`, or `FragmentSpread` nodes) 8 | // 9 | // @note We are essentially executing an operation at compile-type to derive the shape of the result. 10 | // This means we need the following pieces of data available to our type: 11 | // Schema, FragmentDefinitions, Operations, Root Type* 12 | export type Result< 13 | Schema extends Record, 14 | Parent, 15 | Selected extends SelectionSet> | undefined, 16 | > = 17 | // Lists 18 | Parent extends Array | ReadonlyArray 19 | ? ReadonlyArray> 20 | : // Objects 21 | Parent extends Record 22 | ? Selected extends SelectionSet> 23 | ? HasInlineFragment extends Test.Pass 24 | ? SpreadFragments 25 | : { 26 | // @todo cleanup mapped typed field name mapping 27 | readonly [ 28 | F in Selected["selections"][number] as InferName 29 | ]: InferResult; 30 | } 31 | : never 32 | : // Scalars 33 | Parent; 34 | 35 | type InferName = 36 | F extends Field 37 | ? AliasName extends string 38 | ? AliasName 39 | : Name 40 | : never; 41 | 42 | type InferResult< 43 | F, 44 | Schema extends Record, 45 | Parent extends Record 46 | > = 47 | F extends Field 48 | ? Result, SS> 49 | : never 50 | 51 | /* @note support parameterized fields */ 52 | type InferParent, Name extends string> = 53 | Parent[Name] extends (variables: any) => infer T 54 | ? T 55 | : Parent[Name] 56 | 57 | export type SpreadFragments< 58 | Schema extends Record, 59 | Selected extends SelectionSet>, 60 | > = Selected["selections"][number] extends infer Selection 61 | ? Selection extends InlineFragment ? SpreadFragment< 62 | Schema, 63 | Selection, 64 | SelectionSet>> // @bug are we are losing inference here since `SelectionSet<[Field<'id'>]>` works? 65 | > 66 | : never 67 | : never; 68 | 69 | export type SpreadFragment< 70 | Schema extends Record, 71 | Fragment extends InlineFragment, 72 | CommonSelection extends SelectionSet>>, 73 | > = Fragment extends InlineFragment< 74 | NamedType, 75 | infer SelectionSet 76 | > ? Merge< 77 | { __typename: Typename }, 78 | Result< 79 | Schema, 80 | Schema[Typename], 81 | MergeSelectionSets 82 | > 83 | > 84 | : never; 85 | 86 | export type MergeSelectionSets< 87 | A extends SelectionSet>, 88 | B extends SelectionSet>, 89 | > = SelectionSet>; 90 | 91 | type HasInlineFragment | undefined> = T extends SelectionSet 92 | ? L.Includes> 93 | : never; 94 | 95 | type Merge = Omit & N; 96 | 97 | export type SelectionResult = 98 | TSelection extends { toQuery(opts: any): TypedDocumentNode } 99 | ? ResultType 100 | : never; 101 | 102 | -------------------------------------------------------------------------------- /src/Selection.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLSchema } from "graphql/type"; 2 | import { OperationTypeNode, print } from "graphql/language"; 3 | // @note v16+ of graphql-js exposes their own version of a typed DocumentNode 4 | // See https://github.com/dotansimha/graphql-typed-document-node/issues/68 5 | // 6 | // import type { TypedQueryDocumentNode } from "graphql/utilities"; 7 | import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; 8 | 9 | import type * as AST from "./AST"; 10 | import { 11 | fragmentDefinition, 12 | inlineFragment, 13 | namedType, 14 | selectionSet, 15 | operation, 16 | document, 17 | } from "./AST"; 18 | import { Result } from "./Result"; 19 | import { Variables, buildVariableDefinitions } from "./Variables"; 20 | 21 | type Element = T extends Array ? U : never; 22 | 23 | export class Selection< 24 | Schema extends Record, 25 | RootType extends string /* @todo keyof Schema*/, 26 | T extends ReadonlyArray 27 | > extends Array> { 28 | constructor( 29 | public readonly schema: GraphQLSchema, 30 | public readonly type: RootType, 31 | public readonly selections: T 32 | ) { 33 | super(...(selections as unknown as Element[]) /* seems wrong*/); 34 | } 35 | 36 | toSelectionSet(): AST.SelectionSet { 37 | return selectionSet(this.selections); 38 | } 39 | 40 | toFragment( 41 | name: Name 42 | ): AST.FragmentDefinition< 43 | Name, 44 | AST.NamedType, 45 | AST.SelectionSet 46 | > { 47 | return fragmentDefinition( 48 | name, 49 | namedType(this.type), 50 | selectionSet(this.selections) 51 | ); 52 | } 53 | 54 | toInlineFragment(): AST.InlineFragment< 55 | AST.NamedType, 56 | AST.SelectionSet 57 | > { 58 | return inlineFragment(namedType(this.type), selectionSet(this.selections)); 59 | } 60 | 61 | // toOperation? toDocument? 62 | toQuery(options: { 63 | queryName?: string; 64 | useVariables?: boolean; 65 | dropNullInputValues?: boolean; 66 | }): TypedDocumentNode< 67 | Result>, 68 | Variables> 69 | > { 70 | // @todo statically gate? 71 | if ( 72 | this.type !== "Query" && 73 | this.type !== "Mutation" && 74 | this.type !== "Subscription" 75 | ) { 76 | throw new Error("Cannot convert non-root type to query."); 77 | } 78 | 79 | const op = this.type.toLowerCase() as OperationTypeNode; 80 | const selectionSet = this.toSelectionSet(); 81 | const variableDefinitions = buildVariableDefinitions( 82 | this.schema, 83 | op, 84 | selectionSet 85 | ); 86 | 87 | const operationDefinition = operation( 88 | op, 89 | options.queryName ?? "Anonymous", 90 | selectionSet, 91 | variableDefinitions 92 | ); 93 | 94 | return document([operationDefinition]) as TypedDocumentNode< 95 | Result>, 96 | Variables> 97 | >; 98 | } 99 | 100 | // @todo toRequest (converts to node-fetch API compatible `Request` object) 101 | 102 | toString() { 103 | return print(this.toSelectionSet()); 104 | } 105 | } 106 | 107 | export class TypeConditionError extends Error { 108 | constructor(metadata: { selectedType: string; abstractType: string }) { 109 | super( 110 | `"${metadata.selectedType}" is not a valid type of abstract "${metadata.abstractType}" type.` 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Variables.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLSchema, OperationTypeNode } from "graphql"; 2 | import { Kind, visitWithTypeInfo, TypeInfo, visit } from "graphql"; 3 | import type { O, U } from "ts-toolbelt"; 4 | 5 | import { 6 | SelectionSet, 7 | Selection, 8 | Field, 9 | InlineFragment, 10 | Argument, 11 | Variable, 12 | VariableDefinition, 13 | variable, 14 | variableDefinition, 15 | NamedType, 16 | operation, 17 | } from "./AST"; 18 | 19 | export const $ = (name: Name): Variable => 20 | variable(name); 21 | 22 | export const buildVariableDefinitions = >( 23 | schema: GraphQLSchema, 24 | root: OperationTypeNode, 25 | selectionSet: T 26 | ): Array> => { 27 | const variableDefinitions: VariableDefinition[] = []; 28 | const typeInfo = new TypeInfo(schema); 29 | 30 | // @note need to wrap selectionset in an operation (root) for TypeInfo to track correctly 31 | const operationDefinition = operation( 32 | root, 33 | "", 34 | selectionSet, 35 | variableDefinitions 36 | ); 37 | 38 | const visitor = visitWithTypeInfo(typeInfo, { 39 | [Kind.ARGUMENT]: (node) => { 40 | const type = typeInfo.getArgument()?.astNode?.type!; 41 | 42 | if (node.value.kind === "Variable") { 43 | // define the `VariableDefinition` 44 | variableDefinitions.push(variableDefinition(node.value, type)); 45 | } 46 | }, 47 | }); 48 | 49 | // @todo return from here 50 | visit(operationDefinition, visitor); 51 | 52 | return variableDefinitions; 53 | }; 54 | 55 | // @note Traverse the AST extracting `Argument` nodes w/ values of `Variable`. 56 | // Extract the type the Variable value needs to be against/the Schema. 57 | export type Variables< 58 | Schema extends Record, 59 | RootType extends Record, 60 | S extends SelectionSet> | undefined 61 | > = U.Merge< 62 | undefined extends S 63 | ? {} 64 | : S extends SelectionSet> 65 | ? S["selections"][number] extends infer Selection 66 | ? Selection extends Field 67 | ? O.Merge< 68 | FilterMapArguments, 69 | Variables 70 | > 71 | : Selection extends InlineFragment< 72 | NamedType, 73 | infer FragSelection 74 | > 75 | ? Variables 76 | : {} 77 | : {} 78 | : {} 79 | >; 80 | 81 | // @note filter on `Argument`'s that have values of `Variable` 82 | // @todo replace with actual `Filter` type 83 | type FilterMapArguments< 84 | Type extends Record, 85 | FieldName extends string, 86 | FieldArgs extends Array> | undefined 87 | > = FieldArgs extends Array> 88 | ? ArgValue extends Variable 89 | ? Record> 90 | : {} 91 | : {}; 92 | 93 | type VariableType< 94 | Parent extends Record, 95 | Field extends string, 96 | Arg extends string 97 | > = 98 | // see if the field is parameterized 99 | Parent[Field] extends (variables: any) => any 100 | ? Parameters[0] extends infer U // get the `variables` arg 101 | ? // ensure the "variables" parameter is a Record 102 | // @note too-bad JavaScript doesn't support named arguments 103 | U extends Record 104 | ? // exract the cooresponding type for the argument 105 | VarName extends Arg 106 | ? VarType 107 | : never 108 | : never 109 | : never 110 | : never; 111 | -------------------------------------------------------------------------------- /src/__tests__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/src/__tests__/.gitkeep -------------------------------------------------------------------------------- /src/__tests__/Selection.test.ts: -------------------------------------------------------------------------------- 1 | import { Selection } from "../Selection"; 2 | 3 | describe("Selection", () => { 4 | it.todo("is an Array of Selection AST objects"); 5 | it.todo("converts to a SelectionSet AST object"); 6 | it.todo("converts to an InlineFragment AST object"); 7 | it.todo("converts to a NamedFragment AST object"); 8 | it.todo("converts to a string"); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/Variables.test.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, OperationTypeNode } from "graphql"; 2 | 3 | import { 4 | namedType, 5 | nonNull, 6 | field, 7 | argument, 8 | variable, 9 | selectionSet, 10 | variableDefinition, 11 | } from "../AST"; 12 | import { Selection } from "../Selection"; 13 | import { buildVariableDefinitions } from "../Variables"; 14 | 15 | describe("Variables", () => { 16 | it("builds variable definitions", () => { 17 | const schema = buildSchema( 18 | ` 19 | type Query { 20 | hello(name: String!): String! 21 | } 22 | `, 23 | { noLocation: true } 24 | ); 25 | 26 | const selection = new Selection(schema, "Query", [ 27 | field("hello", [argument("name", variable("name"))]), 28 | ]); 29 | 30 | // @note we need a way to get the input type at runtime 31 | const variableDefinitions = buildVariableDefinitions( 32 | schema, 33 | OperationTypeNode.QUERY, 34 | selectionSet(selection) 35 | ); 36 | 37 | expect(variableDefinitions).toEqual([ 38 | variableDefinition(variable("name"), nonNull(namedType("String"))), 39 | ]); 40 | }); 41 | 42 | it("infers nested variable definitions", () => { 43 | const schema = buildSchema( 44 | ` 45 | type Query { 46 | viewer: User! 47 | } 48 | 49 | type User { 50 | id: ID! 51 | friends(limit: Int!): [User!] 52 | } 53 | `, 54 | { noLocation: true } 55 | ); 56 | 57 | const selection = new Selection(schema, "Query", [ 58 | field( 59 | "viewer", 60 | undefined, 61 | selectionSet([ 62 | field( 63 | "friends", 64 | [argument("limit", variable("friendsLimit"))], 65 | selectionSet([field("id")]) 66 | ), 67 | ]) 68 | ), 69 | ]); 70 | 71 | const variableDefinitions = buildVariableDefinitions( 72 | schema, 73 | OperationTypeNode.QUERY, 74 | selectionSet(selection) 75 | ); 76 | 77 | expect(variableDefinitions).toEqual([ 78 | variableDefinition(variable("friendsLimit"), nonNull(namedType("Int"))), 79 | ]); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AST"; 2 | export * from "./Result"; 3 | export * from "./Variables"; 4 | export { Selection as SelectionBuilder, TypeConditionError } from "./Selection"; 5 | -------------------------------------------------------------------------------- /test-d/field-alias.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | import freeze from "deep-freeze"; 3 | import { selectionSet, Result, field } from "../src"; 4 | 5 | interface Schema { 6 | String: string; 7 | Boolean: boolean; 8 | Int: number; 9 | Float: number; 10 | ID: string; 11 | 12 | Query: Query; 13 | User: User; 14 | } 15 | 16 | interface Query { 17 | __typename: "Query"; 18 | userById: User | null; 19 | } 20 | 21 | interface User { 22 | __typename: "User"; 23 | id: string; 24 | firstName: string; 25 | age: number | null; 26 | } 27 | 28 | const selection = selectionSet([ 29 | field("userById", undefined, selectionSet([ 30 | field("firstName").as("name") 31 | ])) 32 | ]) 33 | 34 | type Test = Result; 35 | 36 | expectAssignable( 37 | freeze({ 38 | userById: { 39 | name: "test" 40 | } 41 | }) 42 | ) 43 | 44 | expectAssignable( 45 | freeze({ 46 | userById: null 47 | }) 48 | ) 49 | 50 | -------------------------------------------------------------------------------- /test-d/fragments.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | import { L } from "ts-toolbelt"; 3 | 4 | import { 5 | SelectionSet, 6 | selectionSet, 7 | field, 8 | Field, 9 | namedType, 10 | inlineFragment, 11 | InlineFragment, 12 | MergeSelectionSets, 13 | SpreadFragment, 14 | SpreadFragments, 15 | } from "../src"; 16 | 17 | interface Schema { 18 | String: string; 19 | Boolean: boolean; 20 | Int: number; 21 | Float: number; 22 | ID: string; 23 | 24 | Query: Query; 25 | Node: Node; 26 | Employee: Employee; 27 | Admin: Admin; 28 | } 29 | 30 | interface Query { 31 | __typename: "Query"; 32 | nodes: Node[]; 33 | } 34 | 35 | interface Node { 36 | __typename: "Employee" | "Admin"; 37 | id: string; 38 | } 39 | 40 | interface Employee extends Node { 41 | __typename: "Employee"; 42 | firstName: string; 43 | } 44 | 45 | interface Admin extends Node { 46 | __typename: "Admin"; 47 | badass: boolean; 48 | badgeNumber: number; 49 | } 50 | 51 | const fragment1 = inlineFragment( 52 | namedType<"Employee">("Employee"), 53 | selectionSet([field("firstName")]) 54 | ); 55 | 56 | const fragment2 = inlineFragment( 57 | namedType<"Admin">("Admin"), 58 | selectionSet([field("badass"), field("badgeNumber")]) 59 | ); 60 | 61 | // need to assert const because not function return? 62 | const s = selectionSet([field("id"), fragment1, fragment2] as const); 63 | 64 | type filtered = L.Filter>; 65 | 66 | // working 67 | type TFragment1 = SpreadFragment>; 68 | type TFragment2 = SpreadFragment>; 69 | 70 | expectAssignable({ __typename: "Employee", firstName: "John" }); 71 | expectAssignable({ 72 | __typename: "Admin", 73 | badass: true, 74 | badgeNumber: 69, 75 | }); 76 | 77 | const data = {} as SpreadFragments; 78 | 79 | if (data.__typename === "Admin") { 80 | data.badass; 81 | data.badgeNumber; 82 | } else { 83 | data.firstName; 84 | } 85 | 86 | type merged = MergeSelectionSets< 87 | SelectionSet<[Field<"id">]>, 88 | SelectionSet<[Field<"name">]> 89 | >; 90 | type merged2 = L.Concat<[Field<"id">], [Field<"name">]>; 91 | 92 | const m = [field("id"), field("name")] as merged2; 93 | -------------------------------------------------------------------------------- /test-d/interface.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | 3 | import { selectionSet, field, namedType, inlineFragment, Result } from "../src"; 4 | 5 | interface Schema { 6 | String: string; 7 | Boolean: boolean; 8 | Int: number; 9 | Float: number; 10 | ID: string; 11 | 12 | Query: Query; 13 | Node: Node; 14 | Employee: Employee; 15 | Admin: Admin; 16 | } 17 | 18 | interface Query { 19 | __typename: "Query"; 20 | node: Node | null; 21 | } 22 | 23 | interface Node { 24 | __typename: "Employee" | "Admin"; 25 | id: string; 26 | } 27 | 28 | interface Employee extends Node { 29 | __typename: "Employee"; 30 | firstName: string; 31 | } 32 | 33 | interface Admin extends Node { 34 | __typename: "Admin"; 35 | badass: boolean; 36 | badgeNumber: number; 37 | } 38 | 39 | const selection = selectionSet([ 40 | field( 41 | "node", 42 | undefined, 43 | selectionSet([ 44 | field("__typename"), 45 | field("id"), 46 | 47 | inlineFragment( 48 | namedType<"Employee">("Employee"), 49 | selectionSet([field("firstName")] as const) 50 | ), 51 | 52 | inlineFragment( 53 | namedType<"Admin">("Admin"), 54 | selectionSet([ 55 | field("id"), 56 | field("badass"), 57 | field("badgeNumber"), 58 | ] as const) 59 | ), 60 | ] as const) 61 | ), 62 | ] as const); 63 | 64 | type Test = Result; 65 | 66 | const data = {} as Test; 67 | 68 | if (data.node?.__typename === "Employee") { 69 | data.node.__typename; 70 | data.node.id; 71 | data.node.firstName; 72 | } else if (data.node?.__typename === "Admin") { 73 | data.node.__typename; 74 | data.node.id; 75 | data.node.badass; 76 | data.node.badgeNumber; 77 | } else { 78 | // expect null 79 | data.node; 80 | } 81 | 82 | expectAssignable({ 83 | node: { 84 | __typename: "Employee", 85 | id: "123", 86 | firstName: "Gina", 87 | }, 88 | }); 89 | expectAssignable({ 90 | node: { 91 | __typename: "Admin", 92 | id: "123", 93 | badass: true, 94 | badgeNumber: 69, 95 | }, 96 | }); 97 | -------------------------------------------------------------------------------- /test-d/nested-objects.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | import freeze from "deep-freeze"; 3 | 4 | import { selectionSet, field, Result } from "../src"; 5 | 6 | interface Schema { 7 | String: string; 8 | Boolean: boolean; 9 | Int: number; 10 | Float: number; 11 | ID: string; 12 | 13 | Query: Query; 14 | User: User; 15 | } 16 | 17 | interface Query { 18 | __typename: "Query"; 19 | viewer: User; 20 | friendsByUserId: User[] | null; 21 | } 22 | 23 | interface User { 24 | __typename: "User"; 25 | id: string; 26 | firstName: string; 27 | age: number | null; 28 | friends: User[] | null; 29 | } 30 | 31 | type f = User["friends"] extends Array | null ? true : false; 32 | type isNullable = null extends User["friends"] ? true : false; 33 | 34 | const selection = selectionSet([ 35 | field("viewer", undefined, selectionSet([field("id")])), 36 | field( 37 | "friendsByUserId", 38 | undefined, 39 | selectionSet([ 40 | field("id"), 41 | field("firstName"), 42 | field("age"), 43 | field("friends", undefined, selectionSet([field("id")])), 44 | ]) 45 | ), 46 | ]); 47 | 48 | type Test = Result; 49 | 50 | expectAssignable( 51 | freeze({ 52 | viewer: { 53 | id: "foo", 54 | }, 55 | friendsByUserId: [ 56 | { 57 | id: "foo", 58 | firstName: "Tim", 59 | age: 69, 60 | friends: [{ id: "bar" }], 61 | }, 62 | ], 63 | }) 64 | ); 65 | 66 | expectAssignable( 67 | freeze({ 68 | viewer: { 69 | id: "foo", 70 | }, 71 | friendsByUserId: null, 72 | }) 73 | ); 74 | -------------------------------------------------------------------------------- /test-d/nested-variables.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | import { Variables, selectionSet, field, argument, variable } from "../src"; 3 | 4 | interface Schema { 5 | String: string; 6 | Boolean: boolean; 7 | Int: number; 8 | Float: number; 9 | ID: string; 10 | 11 | Query: Query; 12 | User: User; 13 | } 14 | 15 | interface Query { 16 | __typename: "Query"; 17 | viewer: User; 18 | user(variables: { id: string }): User | null; 19 | } 20 | 21 | interface User { 22 | id: string; 23 | friends(variables: { limit: number | undefined }): User[]; 24 | } 25 | 26 | const selection = selectionSet([ 27 | field("user", [argument("id", variable("id"))], selectionSet([field("id")])), 28 | field( 29 | "viewer", 30 | undefined, 31 | selectionSet([ 32 | field( 33 | "friends", 34 | [argument("limit", variable("limit"))], 35 | selectionSet([field("id")]) 36 | ), 37 | ]) 38 | ), 39 | ]); 40 | 41 | type Test = Variables; 42 | 43 | expectAssignable({ 44 | id: "abc", 45 | limit: 5, 46 | }); 47 | -------------------------------------------------------------------------------- /test-d/optional-variables.test-d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/test-d/optional-variables.test-d.ts -------------------------------------------------------------------------------- /test-d/parameterized.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import freeze from "deep-freeze"; 3 | 4 | import { selectionSet, field, Result } from "../src"; 5 | 6 | interface Schema { 7 | Query: Query; 8 | } 9 | 10 | interface Query { 11 | __typename: "Query"; 12 | hello(variables: { name: string }): string; 13 | } 14 | 15 | const selection = selectionSet([field("__typename"), field("hello")]); 16 | 17 | type Test = Result; 18 | 19 | expectType(freeze({ __typename: "Query", hello: "foo" })); 20 | -------------------------------------------------------------------------------- /test-d/simple-object.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | import freeze from "deep-freeze"; 3 | import { O } from "ts-toolbelt"; 4 | 5 | import { selectionSet, field, Result } from "../src"; 6 | 7 | interface Schema { 8 | String: string; 9 | Boolean: boolean; 10 | Int: number; 11 | Float: number; 12 | ID: string; 13 | 14 | Query: Query; 15 | User: User; 16 | } 17 | 18 | interface Query { 19 | __typename: "Query"; 20 | viewer: User; 21 | } 22 | 23 | interface User { 24 | __typename: "User"; 25 | id: string; 26 | firstName: string; 27 | age: number | null; 28 | pronouns: string[]; 29 | } 30 | 31 | const selection = selectionSet([ 32 | field( 33 | "viewer", 34 | undefined, 35 | selectionSet([ 36 | field("id"), 37 | field("firstName"), 38 | field("age"), 39 | field("pronouns"), 40 | ]) 41 | ), 42 | ]); 43 | 44 | type Test = Result; 45 | 46 | expectAssignable( 47 | freeze({ viewer: { id: "foo", firstName: "Tim", age: 69, pronouns: [] } }) 48 | ); 49 | expectAssignable( 50 | freeze({ 51 | viewer: { id: "foo", firstName: "Tim", age: null, pronouns: ["xenon"] }, 52 | }) 53 | ); 54 | -------------------------------------------------------------------------------- /test-d/simple-scalar.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import freeze from "deep-freeze"; 3 | 4 | import { selectionSet, field, Result } from "../src"; 5 | 6 | interface Schema { 7 | Query: Query; 8 | } 9 | 10 | interface Query { 11 | __typename: "Query"; 12 | hello: string; 13 | } 14 | 15 | const selection = selectionSet([field("__typename"), field("hello")]); 16 | 17 | type Test = Result; 18 | 19 | expectType(freeze({ __typename: "Query", hello: "foo" })); 20 | -------------------------------------------------------------------------------- /test-d/union.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | 3 | import { selectionSet, field, namedType, inlineFragment, Result } from "../src"; 4 | 5 | interface Schema { 6 | String: string; 7 | Boolean: boolean; 8 | Int: number; 9 | Float: number; 10 | ID: string; 11 | 12 | Query: Query; 13 | SearchResult: SearchResult; 14 | Author: Author; 15 | Book: Book; 16 | } 17 | 18 | interface Query { 19 | __typename: "Query"; 20 | search: (SearchResult | null)[] | null; 21 | } 22 | 23 | interface Author { 24 | __typename: "Author"; 25 | name: string; 26 | } 27 | 28 | interface Book { 29 | __typename: "Book"; 30 | title: string; 31 | } 32 | 33 | type SearchResult = Author | Book; 34 | 35 | const selection = selectionSet([ 36 | field( 37 | "search", 38 | undefined, 39 | selectionSet([ 40 | field("__typename"), 41 | 42 | inlineFragment( 43 | namedType<"Author">("Author"), 44 | selectionSet([field("name")] as const) 45 | ), 46 | 47 | inlineFragment( 48 | namedType<"Book">("Book"), 49 | selectionSet([field("title")] as const) 50 | ), 51 | ] as const) 52 | ), 53 | ] as const); 54 | 55 | type Test = Result; 56 | 57 | const data = {} as Test; 58 | const first = data.search?.[0]; 59 | 60 | if (first?.__typename === "Author") { 61 | first.__typename; 62 | first.name; 63 | } else if (first?.__typename === "Book") { 64 | first.__typename; 65 | first.title; 66 | } else { 67 | // expect null or undefined 68 | first; 69 | } 70 | 71 | expectAssignable({ 72 | search: null, 73 | }); 74 | expectAssignable({ 75 | search: [], 76 | }); 77 | expectAssignable({ 78 | search: [null], 79 | }); 80 | expectAssignable({ 81 | search: [ 82 | { 83 | __typename: "Author", 84 | name: "John", 85 | }, 86 | ], 87 | }); 88 | expectAssignable({ 89 | search: [ 90 | { 91 | __typename: "Book", 92 | title: "Holy Bible", 93 | }, 94 | ], 95 | }); 96 | expectAssignable({ 97 | search: [ 98 | { 99 | __typename: "Book", 100 | title: "Holy Bible", 101 | }, 102 | null, 103 | ], 104 | }); 105 | -------------------------------------------------------------------------------- /test-d/variables-nested-in-fragments.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from "tsd"; 2 | 3 | import { 4 | selectionSet, 5 | field, 6 | argument, 7 | variable, 8 | inlineFragment, 9 | namedType, 10 | Variables, 11 | Result, 12 | } from "../src"; 13 | 14 | interface Schema { 15 | String: string; 16 | Boolean: boolean; 17 | Int: number; 18 | Float: number; 19 | ID: string; 20 | 21 | Query: Query; 22 | User: User; 23 | 24 | Unit: Unit; 25 | } 26 | 27 | interface Query { 28 | __typename: "Query"; 29 | viewer: User; 30 | } 31 | 32 | enum Unit { 33 | FEET = "FEET", 34 | METERS = "METERS", 35 | } 36 | 37 | interface User { 38 | __typename: "User"; 39 | id: string; 40 | // @todo modify Result's SpreadFragment's to support parameterized fields 41 | height(variables: { unit: Unit }): number; 42 | } 43 | 44 | const selection = selectionSet([ 45 | field( 46 | "viewer", 47 | undefined, 48 | selectionSet([ 49 | field("id"), 50 | 51 | inlineFragment( 52 | namedType<"User">("User"), 53 | selectionSet([ 54 | field("height", [argument("unit", variable("heightUnit"))]), 55 | ] as const) 56 | ), 57 | ] as const) 58 | ), 59 | ]); 60 | 61 | type Test = Variables; 62 | type Test2 = Result; 63 | 64 | expectAssignable({ 65 | heightUnit: Unit.FEET, 66 | }); 67 | -------------------------------------------------------------------------------- /test-d/variables.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | 3 | import { Variables, selectionSet, field, argument, variable } from "../src"; 4 | 5 | interface Schema { 6 | String: string; 7 | Boolean: boolean; 8 | Int: number; 9 | Float: number; 10 | ID: string; 11 | 12 | Query: Query; 13 | } 14 | 15 | interface Query { 16 | __typename: "Query"; 17 | hello(variables: { name: string }): string; 18 | } 19 | 20 | const selection = selectionSet([ 21 | field("hello", [argument("name", variable("foo"))]), 22 | ]); 23 | 24 | type Test = Variables; 25 | 26 | expectType({ 27 | foo: "world", 28 | }); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "examples/**/*", "__tests__/**/*", "test-d/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "noEmit": true, 6 | "rootDir": ".", 7 | "outDir": "dist", 8 | "sourceMap": true, 9 | "target": "ES2020", 10 | "module": "commonjs", 11 | "noUnusedLocals": false, 12 | "allowJs": false, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "moduleResolution": "node", 18 | "strictPropertyInitialization": false, 19 | "skipLibCheck": true, 20 | "incremental": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "lib": ["ES2020"] 23 | } 24 | } -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["src/**/*_test.ts", "src/**/*.test.ts"], 5 | "compilerOptions": { 6 | "rootDir": "src", 7 | "outDir": "dist", 8 | "noEmit": false, 9 | "sourceMap": true, 10 | } 11 | } -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'TQL', 10 | tagline: 'A GraphQL query builder for TypeScript. Avoid the pain of codegen.', 11 | url: 'https://tql.dev', 12 | baseUrl: '/', 13 | onBrokenLinks: 'throw', 14 | onBrokenMarkdownLinks: 'warn', 15 | favicon: 'img/favicon.ico', 16 | organizationName: 'timkendall', // Usually your GitHub org/user name. 17 | projectName: 'tql', // Usually your repo name. 18 | 19 | presets: [ 20 | [ 21 | '@docusaurus/preset-classic', 22 | /** @type {import('@docusaurus/preset-classic').Options} */ 23 | ({ 24 | docs: { 25 | sidebarPath: require.resolve('./sidebars.js'), 26 | // Please change this to your repo. 27 | editUrl: 'https://github.com/timkendall/tql/edit/master/website/', 28 | path: '../docs' 29 | }, 30 | blog: false, 31 | theme: { 32 | customCss: require.resolve('./src/css/custom.css'), 33 | }, 34 | }), 35 | ], 36 | ], 37 | 38 | themeConfig: 39 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 40 | ({ 41 | gtag: { 42 | trackingID: 'G-4GY1ML0FR6', 43 | // Optional fields. 44 | anonymizeIP: true, // Should IPs be anonymized? 45 | }, 46 | navbar: { 47 | title: 'TQL', 48 | // logo: { 49 | // alt: 'My Site Logo', 50 | // src: 'img/logo.svg', 51 | // }, 52 | items: [ 53 | { 54 | type: 'doc', 55 | docId: 'quickstart/installation', 56 | position: 'left', 57 | label: 'Docs', 58 | }, 59 | ], 60 | }, 61 | footer: { 62 | style: 'dark', 63 | links: [ 64 | { 65 | title: 'Docs', 66 | items: [ 67 | { 68 | label: 'Quickstart', 69 | to: '/docs/quickstart/installation', 70 | }, 71 | ], 72 | }, 73 | { 74 | title: 'Community', 75 | items: [ 76 | { 77 | label: 'GitHub', 78 | href: 'https://github.com/timkendall/tql', 79 | }, 80 | ], 81 | } 82 | ], 83 | copyright: `Copyright © ${new Date().getFullYear()} Tim Kendall. Built with Docusaurus.`, 84 | }, 85 | prism: { 86 | theme: lightCodeTheme, 87 | darkTheme: darkCodeTheme, 88 | }, 89 | }), 90 | }; 91 | 92 | module.exports = config; 93 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "2.0.0-beta.9", 18 | "@docusaurus/preset-classic": "2.0.0-beta.9", 19 | "@mdx-js/react": "^1.6.21", 20 | "@svgr/webpack": "^5.5.0", 21 | "clsx": "^1.1.1", 22 | "file-loader": "^6.2.0", 23 | "prism-react-renderer": "^1.2.1", 24 | "react": "^17.0.1", 25 | "react-dom": "^17.0.1", 26 | "url-loader": "^4.1.1" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | default: [ 17 | { 18 | type: 'category', 19 | label: 'Quickstart', 20 | collapsed: false, 21 | items: [ 22 | { 23 | type: 'doc', 24 | label: 'Installation', 25 | id: 'quickstart/installation' 26 | }, 27 | { 28 | type: 'doc', 29 | label: 'Codegen', 30 | id: 'quickstart/codegen' 31 | }, 32 | { 33 | type: 'doc', 34 | label: 'Demo', 35 | id: 'quickstart/demo' 36 | }, 37 | ] 38 | }, 39 | { 40 | type: 'doc', 41 | label: 'Operations', 42 | id: 'operations' 43 | }, 44 | { 45 | type: 'doc', 46 | label: 'Variables', 47 | id: 'variables' 48 | }, 49 | { 50 | type: 'category', 51 | label: 'Fragments', 52 | items: [ 53 | { 54 | type: 'doc', 55 | label: 'Inline', 56 | id: 'fragments/inline' 57 | }, 58 | { 59 | type: 'doc', 60 | label: 'Named', 61 | id: 'fragments/named' 62 | }, 63 | ] 64 | }, 65 | { 66 | type: 'doc', 67 | label: 'Directives', 68 | id: 'directives' 69 | }, 70 | { 71 | type: 'category', 72 | label: 'Client Examples', 73 | collapsed: false, 74 | items: [ 75 | { 76 | type: 'doc', 77 | label: '@apollo/client', 78 | id: 'client-examples/apollo' 79 | }, 80 | { 81 | type: 'doc', 82 | label: 'graphql-request', 83 | id: 'client-examples/graphql-request' 84 | }, 85 | { 86 | type: 'doc', 87 | label: 'urql', 88 | id: 'client-examples/urql' 89 | }, 90 | ] 91 | }, 92 | { 93 | type: 'category', 94 | label: 'API', 95 | items: [ 96 | { 97 | type: 'doc', 98 | label: 'Selection', 99 | id: 'api/selection' 100 | }, 101 | ] 102 | }, 103 | ] 104 | }; 105 | 106 | module.exports = sidebars; 107 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './HomepageFeatures.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: 'Easy to Use', 8 | Svg: require('../../static/img/undraw_docusaurus_mountain.svg').default, 9 | description: ( 10 | <> 11 | Docusaurus was designed from the ground up to be easily installed and 12 | used to get your website up and running quickly. 13 | 14 | ), 15 | }, 16 | { 17 | title: 'Focus on What Matters', 18 | Svg: require('../../static/img/undraw_docusaurus_tree.svg').default, 19 | description: ( 20 | <> 21 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 22 | ahead and move your docs into the docs directory. 23 | 24 | ), 25 | }, 26 | { 27 | title: 'Powered by React', 28 | Svg: require('../../static/img/undraw_docusaurus_react.svg').default, 29 | description: ( 30 | <> 31 | Extend or customize your website layout by reusing React. Docusaurus can 32 | be extended while reusing the same header and footer. 33 | 34 | ), 35 | }, 36 | ]; 37 | 38 | function Feature({Svg, title, description}) { 39 | return ( 40 |
41 |
42 | 43 |
44 |
45 |

{title}

46 |

{description}

47 |
48 |
49 | ); 50 | } 51 | 52 | export default function HomepageFeatures() { 53 | return ( 54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => ( 58 | 59 | ))} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #25c2a0; 10 | --ifm-color-primary-dark: rgb(33, 175, 144); 11 | --ifm-color-primary-darker: rgb(31, 165, 136); 12 | --ifm-color-primary-darkest: rgb(26, 136, 112); 13 | --ifm-color-primary-light: rgb(70, 203, 174); 14 | --ifm-color-primary-lighter: rgb(102, 212, 189); 15 | --ifm-color-primary-lightest: rgb(146, 224, 208); 16 | --ifm-code-font-size: 95%; 17 | } 18 | 19 | .docusaurus-highlight-code-line { 20 | background-color: rgba(0, 0, 0, 0.1); 21 | display: block; 22 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 23 | padding: 0 var(--ifm-pre-padding); 24 | } 25 | 26 | html[data-theme='dark'] .docusaurus-highlight-code-line { 27 | background-color: rgba(0, 0, 0, 0.3); 28 | } 29 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import styles from './index.module.css'; 7 | import HomepageFeatures from '../components/HomepageFeatures'; 8 | 9 | function HomepageHeader() { 10 | const {siteConfig} = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 |

{siteConfig.title}

15 |

{siteConfig.tagline}

16 |
17 | 20 | Docusaurus Tutorial - 5min ⏱️ 21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | 28 | export default function Home() { 29 | const {siteConfig} = useDocusaurusContext(); 30 | return ( 31 | 34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /website/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/website/static/img/docusaurus.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/img/tutorial/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/website/static/img/tutorial/docsVersionDropdown.png -------------------------------------------------------------------------------- /website/static/img/tutorial/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timkendall/tql/a87d1de76d2232355dcf8e6a590884057c8d17c2/website/static/img/tutorial/localeDropdown.png -------------------------------------------------------------------------------- /website/static/img/undraw_docusaurus_mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /website/static/img/undraw_docusaurus_react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /website/static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | docu_tree --------------------------------------------------------------------------------