From 543ffdfa08032f97ca7ae087498f1ef387927bd4 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sun, 12 May 2024 09:40:58 -0400 Subject: [PATCH] feat(graffle): static exec and req funcs (#851) --- .github/workflows/pr.yml | 2 +- package.json | 3 +- pnpm-lock.yaml | 263 +++++++++++++++++- src/entrypoints/alpha/main.ts | 2 + src/layers/0_functions/execute.ts | 32 +++ src/layers/0_functions/request.ts | 47 ++++ src/layers/0_functions/requestOrExecute.ts | 23 ++ src/layers/0_functions/types.ts | 8 + .../5_client/client.customScalar.test.ts | 43 +-- src/layers/5_client/client.raw.test.ts | 4 +- .../5_client/client.returnMode.test-d.ts | 10 +- src/layers/5_client/client.returnMode.test.ts | 8 +- src/layers/5_client/client.ts | 99 +++---- src/layers/5_client/types.ts | 5 + src/lib/graphql.ts | 4 +- src/lib/graphqlHTTP.ts | 46 +++ tests/_/helpers.ts | 23 ++ vite.config.ts | 3 - 18 files changed, 519 insertions(+), 106 deletions(-) create mode 100644 src/layers/0_functions/execute.ts create mode 100644 src/layers/0_functions/request.ts create mode 100644 src/layers/0_functions/requestOrExecute.ts create mode 100644 src/layers/0_functions/types.ts create mode 100644 src/layers/5_client/types.ts create mode 100644 src/lib/graphqlHTTP.ts create mode 100644 tests/_/helpers.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 790cd0516..48ead38cf 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: node: [18, 20] - environment: [happy-dom, node] + environment: [jsdom, node] name: Node ${{ matrix.node }} @env ${{matrix.environment}} steps: - uses: actions/checkout@v4 diff --git a/package.json b/package.json index e62639a56..901e12801 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "build": "pnpm clean && pnpm tsc --project tsconfig.build.json && chmod +x ./build/cli/generate.js", "clean": "tsc --build --clean && rm -rf build", "test": "vitest", + "test:web": "vitest --environment jsdom", "test:types": "vitest --typecheck", "test:coverage": "pnpm test -- --coverage", "release:stable": "dripip stable", @@ -116,7 +117,7 @@ "graphql": "^16.8.1", "graphql-scalars": "^1.23.0", "graphql-tag": "^2.12.6", - "happy-dom": "^14.7.1", + "jsdom": "^24.0.0", "json-bigint": "^1.0.0", "tsx": "^4.7.3", "type-fest": "^4.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d42644527..01e585b25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,9 +108,9 @@ importers: graphql-tag: specifier: ^2.12.6 version: 2.12.6(graphql@16.8.1) - happy-dom: - specifier: ^14.7.1 - version: 14.10.1 + jsdom: + specifier: ^24.0.0 + version: 24.0.0 json-bigint: specifier: ^1.0.0 version: 1.0.0 @@ -128,7 +128,7 @@ importers: version: 7.8.0(eslint@9.2.0)(typescript@5.4.5) vitest: specifier: ^1.5.2 - version: 1.6.0(@types/node@20.12.11)(happy-dom@14.10.1) + version: 1.6.0(@types/node@20.12.11)(happy-dom@14.10.1)(jsdom@24.0.0) packages: @@ -983,6 +983,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1255,10 +1259,18 @@ packages: cssfilter@0.0.10: resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + cssstyle@4.0.1: + resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} + engines: {node: '>=18'} + dashdash@1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -1296,6 +1308,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1725,6 +1740,10 @@ packages: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} engines: {node: '>= 0.12'} + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -1892,6 +1911,10 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + htmlparser2@7.2.0: resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} @@ -1899,10 +1922,18 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} + https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -1911,6 +1942,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -2030,6 +2065,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -2096,6 +2134,15 @@ packages: engines: {node: '>=10'} hasBin: true + jsdom@24.0.0: + resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -2359,6 +2406,9 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nwsapi@2.2.9: + resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==} + oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -2443,6 +2493,9 @@ packages: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2556,6 +2609,9 @@ packages: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2625,6 +2681,9 @@ packages: engines: {node: '>= 6'} deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2656,6 +2715,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2673,6 +2735,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -2825,6 +2891,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2851,9 +2920,17 @@ packages: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + traverse@0.6.9: resolution: {integrity: sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==} engines: {node: '>= 0.4'} @@ -2983,6 +3060,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2993,6 +3074,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3092,6 +3176,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -3099,10 +3187,22 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -3142,6 +3242,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xss@1.0.15: resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} engines: {node: '>= 0.10.0'} @@ -4131,6 +4250,12 @@ snapshots: acorn@8.11.3: {} + agent-base@7.1.1: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4447,10 +4572,19 @@ snapshots: cssfilter@0.0.10: {} + cssstyle@4.0.1: + dependencies: + rrweb-cssom: 0.6.0 + dashdash@1.14.1: dependencies: assert-plus: 1.0.0 + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + data-view-buffer@1.0.1: dependencies: call-bind: 1.0.7 @@ -4481,6 +4615,8 @@ snapshots: dependencies: ms: 2.1.2 + decimal.js@10.4.3: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -5115,6 +5251,12 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + format@0.2.2: {} forwarded@0.2.0: {} @@ -5239,6 +5381,7 @@ snapshots: entities: 4.5.0 webidl-conversions: 7.0.0 whatwg-mimetype: 3.0.0 + optional: true har-schema@2.0.0: {} @@ -5271,6 +5414,10 @@ snapshots: hosted-git-info@2.8.9: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + htmlparser2@7.2.0: dependencies: domelementtype: 2.3.0 @@ -5286,18 +5433,36 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 jsprim: 1.4.2 sshpk: 1.18.0 + https-proxy-agent@7.0.4: + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + human-signals@5.0.0: {} iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.1: {} import-fresh@3.3.0: @@ -5393,6 +5558,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -5458,6 +5625,34 @@ snapshots: jsdoctypeparser@9.0.0: {} + jsdom@24.0.0: + dependencies: + cssstyle: 4.0.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.9 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.17.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + json-bigint@1.0.0: dependencies: bignumber.js: 9.1.2 @@ -5759,6 +5954,8 @@ snapshots: dependencies: path-key: 4.0.0 + nwsapi@2.2.9: {} + oauth-sign@0.9.0: {} object-assign@4.1.1: {} @@ -5850,6 +6047,10 @@ snapshots: error-ex: 1.3.2 json-parse-better-errors: 1.0.2 + parse5@7.1.2: + dependencies: + entities: 4.5.0 + parseurl@1.3.3: {} path-exists@3.0.0: {} @@ -5933,6 +6134,8 @@ snapshots: qs@6.5.3: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} ramda@0.27.2: {} @@ -6030,6 +6233,8 @@ snapshots: tunnel-agent: 0.6.0 uuid: 3.4.0 + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -6075,6 +6280,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.17.2 fsevents: 2.3.3 + rrweb-cssom@0.6.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6096,6 +6303,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@5.7.2: {} semver@6.3.1: {} @@ -6280,6 +6491,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + text-table@0.2.0: {} tinybench@2.8.0: {} @@ -6299,8 +6512,19 @@ snapshots: psl: 1.9.0 punycode: 2.3.1 + tough-cookie@4.1.4: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + traverse@0.6.9: dependencies: gopd: 1.0.1 @@ -6456,6 +6680,8 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: {} + unpipe@1.0.0: {} update-section@0.3.3: {} @@ -6464,6 +6690,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -6527,7 +6758,7 @@ snapshots: '@types/node': 20.12.11 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.12.11)(happy-dom@14.10.1): + vitest@1.6.0(@types/node@20.12.11)(happy-dom@14.10.1)(jsdom@24.0.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -6552,6 +6783,7 @@ snapshots: optionalDependencies: '@types/node': 20.12.11 happy-dom: 14.10.1 + jsdom: 24.0.0 transitivePeerDependencies: - less - lightningcss @@ -6561,12 +6793,27 @@ snapshots: - supports-color - terser + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -6617,6 +6864,12 @@ snapshots: wrappy@1.0.2: {} + ws@8.17.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xss@1.0.15: dependencies: commander: 2.20.3 diff --git a/src/entrypoints/alpha/main.ts b/src/entrypoints/alpha/main.ts index a8011ec05..4620dc77e 100644 --- a/src/entrypoints/alpha/main.ts +++ b/src/entrypoints/alpha/main.ts @@ -1 +1,3 @@ +export { execute } from '../../layers/0_functions/execute.js' +export { request } from '../../layers/0_functions/request.js' export * as Graffle from '../../layers/5_client/client.js' diff --git a/src/layers/0_functions/execute.ts b/src/layers/0_functions/execute.ts new file mode 100644 index 000000000..141153cac --- /dev/null +++ b/src/layers/0_functions/execute.ts @@ -0,0 +1,32 @@ +import type { ExecutionResult, GraphQLSchema } from 'graphql' +import { execute as graphqlExecute, graphql } from 'graphql' +import type { BaseInput } from './types.js' + +interface Input extends BaseInput { + schema: GraphQLSchema +} + +export const execute = async (input: Input): Promise> => { + switch (typeof input.document) { + case `string`: { + return await graphql({ + schema: input.schema, + source: input.document, + // contextValue: createContextValue(), // todo + variableValues: input.variables, + operationName: input.operationName, + }) + } + case `object`: { + return await graphqlExecute({ + schema: input.schema, + document: input.document, + // contextValue: createContextValue(), // todo + variableValues: input.variables, + operationName: input.operationName, + }) + } + default: + throw new Error(`Unsupported GraphQL document type: ${String(document)}`) + } +} diff --git a/src/layers/0_functions/request.ts b/src/layers/0_functions/request.ts new file mode 100644 index 000000000..fc666f05a --- /dev/null +++ b/src/layers/0_functions/request.ts @@ -0,0 +1,47 @@ +import type { ExecutionResult } from 'graphql' +import { print } from 'graphql' +import { parseExecutionResult } from '../../lib/graphqlHTTP.js' +import { CONTENT_TYPE_GQL } from '../../lib/http.js' +import type { BaseInput } from './types.js' + +export type URLInput = URL | string + +interface Input extends BaseInput { + url: URLInput + headers?: HeadersInit +} + +/** + * @see https://graphql.github.io/graphql-over-http/draft/ + */ +export const request = async (input: Input): Promise => { + const documentEncoded = typeof input.document === `string` ? input.document : print(input.document) + + const body = { + query: documentEncoded, + variables: input.variables, + operationName: input.operationName, + } + + const bodyEncoded = JSON.stringify(body) + + const requestObject = new Request(input.url, { + method: `POST`, + headers: new Headers({ + 'content-type': CONTENT_TYPE_GQL, + ...Object.fromEntries(new Headers(input.headers).entries()), + }), + body: bodyEncoded, + }) + + const response = await fetch(requestObject) + + if (!response.ok) { + throw new Error(`Request to GraphQL endpoint failed. (Status: ${String(response.status)})`) + } + + const json = await response.json() as object + const result = parseExecutionResult(json) + + return result +} diff --git a/src/layers/0_functions/requestOrExecute.ts b/src/layers/0_functions/requestOrExecute.ts new file mode 100644 index 000000000..b4978ac76 --- /dev/null +++ b/src/layers/0_functions/requestOrExecute.ts @@ -0,0 +1,23 @@ +import type { ExecutionResult, GraphQLSchema } from 'graphql' +import { execute } from './execute.js' +import type { URLInput } from './request.js' +import { request } from './request.js' +import type { BaseInput } from './types.js' + +export type SchemaInput = URLInput | GraphQLSchema + +export interface Input extends BaseInput { + schema: SchemaInput +} + +export const requestOrExecute = async ( + input: Input, +): Promise => { + const { schema, ...baseInput } = input + + if (schema instanceof URL || typeof schema === `string`) { + return await request({ url: schema, ...baseInput }) + } + + return await execute({ schema, ...baseInput }) +} diff --git a/src/layers/0_functions/types.ts b/src/layers/0_functions/types.ts new file mode 100644 index 000000000..e18503eea --- /dev/null +++ b/src/layers/0_functions/types.ts @@ -0,0 +1,8 @@ +import type { StandardScalarVariables } from '../../lib/graphql.js' +import type { DocumentInput, OperationNameInput } from '../5_client/types.js' + +export interface BaseInput { + document: DocumentInput + variables?: StandardScalarVariables + operationName?: OperationNameInput +} diff --git a/src/layers/5_client/client.customScalar.test.ts b/src/layers/5_client/client.customScalar.test.ts index 8e5602a0f..a768976ce 100644 --- a/src/layers/5_client/client.customScalar.test.ts +++ b/src/layers/5_client/client.customScalar.test.ts @@ -1,6 +1,7 @@ /* eslint-disable */ -import { describe, expect, test } from 'vitest' +import { describe, expect } from 'vitest' import { db } from '../../../tests/_/db.js' +import { createResponse, test } from '../../../tests/_/helpers.js' import { Graffle } from '../../../tests/_/schema/generated/__.js' import { setupMockServer } from '../../../tests/legacy/__helpers.js' @@ -11,50 +12,50 @@ const date1Encoded = db.date1.toISOString() const client = () => Graffle.create({ schema: ctx.url }) describe(`output`, () => { - test(`query field`, async () => { - ctx.res({ body: { data: { date: date0Encoded } } }) + test(`query field`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { date: date0Encoded } })) expect(await client().query.$batch({ date: true })).toEqual({ date: db.date0 }) }) - test(`query field in non-null`, async () => { - ctx.res({ body: { data: { dateNonNull: date0Encoded } } }) + test(`query field in non-null`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateNonNull: date0Encoded } })) expect(await client().query.$batch({ dateNonNull: true })).toEqual({ dateNonNull: db.date0 }) }) - test(`query field in list`, async () => { - ctx.res({ body: { data: { dateList: [0, 1] } } }) + test(`query field in list`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateList: [0, 1] } })) expect(await client().query.$batch({ dateList: true })).toEqual({ dateList: [db.date0, new Date(1)] }) }) - test(`query field in list non-null`, async () => { - ctx.res({ body: { data: { dateList: [0, 1] } } }) + test(`query field in list non-null`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateList: [0, 1] } })) expect(await client().query.$batch({ dateList: true })).toEqual({ dateList: [db.date0, new Date(1)] }) }) - test(`object field`, async () => { - ctx.res({ body: { data: { dateObject1: { date1: 0 } } } }) + test(`object field`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateObject1: { date1: 0 } } })) expect(await client().query.$batch({ dateObject1: { date1: true } })).toEqual({ dateObject1: { date1: db.date0 }, }) }) - test(`object field in interface`, async () => { - ctx.res({ body: { data: { dateInterface1: { date1: 0 } } } }) + test(`object field in interface`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateInterface1: { date1: 0 } } })) expect(await client().query.$batch({ dateInterface1: { date1: true } })).toEqual({ dateInterface1: { date1: db.date0 }, }) }) describe(`object field in union`, () => { - test(`case 1 with __typename`, async () => { - ctx.res({ body: { data: { dateUnion: { __typename: `DateObject1`, date1: 0 } } } }) + test(`case 1 with __typename`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateUnion: { __typename: `DateObject1`, date1: 0 } } })) expect(await client().query.$batch({ dateUnion: { __typename: true, onDateObject1: { date1: true } } })) .toEqual({ dateUnion: { __typename: `DateObject1`, date1: db.date0 }, }) }) - test(`case 1 without __typename`, async () => { - ctx.res({ body: { data: { dateUnion: { date1: date0Encoded } } } }) + test(`case 1 without __typename`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateUnion: { date1: date0Encoded } } })) expect(await client().query.$batch({ dateUnion: { onDateObject1: { date1: true } } })).toEqual({ dateUnion: { date1: db.date0 }, }) }) - test(`case 2`, async () => { - ctx.res({ body: { data: { dateUnion: { date2: date0Encoded } } } }) + test(`case 2`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateUnion: { date2: date0Encoded } } })) expect( await client().query.$batch({ dateUnion: { onDateObject1: { date1: true }, onDateObject2: { date2: true } }, @@ -62,8 +63,8 @@ describe(`output`, () => { ) .toEqual({ dateUnion: { date2: db.date0 } }) }) - test(`case 2 miss`, async () => { - ctx.res({ body: { data: { dateUnion: null } } }) + test(`case 2 miss`, async ({ fetch }) => { + fetch.mockResolvedValueOnce(createResponse({ data: { dateUnion: null } })) expect(await client().query.$batch({ dateUnion: { onDateObject1: { date1: true } } })).toEqual({ dateUnion: null, }) // dprint-ignore diff --git a/src/layers/5_client/client.raw.test.ts b/src/layers/5_client/client.raw.test.ts index d4de8c643..8dbfc43df 100644 --- a/src/layers/5_client/client.raw.test.ts +++ b/src/layers/5_client/client.raw.test.ts @@ -7,13 +7,13 @@ import { schema } from '../../../tests/_/schema/schema.js' const graffle = Graffle.create({ schema }) test(`.rawOrThrow() throws if errors array non-empty`, async () => { - await expect(graffle.rawOrThrow(`query {}`)).rejects.toMatchInlineSnapshot( + await expect(graffle.rawOrThrow({ document: `query {}` })).rejects.toMatchInlineSnapshot( `[ContextualAggregateError: One or more errors in the execution result.]`, ) }) test(`.raw() returns errors in array`, async () => { - await expect(graffle.raw(`query {}`)).resolves.toMatchInlineSnapshot(` + await expect(graffle.raw({ document: `query {}` })).resolves.toMatchInlineSnapshot(` { "errors": [ [GraphQLError: Syntax Error: Expected Name, found "}".], diff --git a/src/layers/5_client/client.returnMode.test-d.ts b/src/layers/5_client/client.returnMode.test-d.ts index 45effc644..71d935b0a 100644 --- a/src/layers/5_client/client.returnMode.test-d.ts +++ b/src/layers/5_client/client.returnMode.test-d.ts @@ -23,7 +23,7 @@ describe('default is data', () => { await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | {__typename: "ErrorOne"} | {__typename: "ErrorTwo"} | null>() }) test(`raw`, async () => { - expectTypeOf(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf() + expectTypeOf(graffle.raw({ document:'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() }) }) @@ -46,7 +46,7 @@ describe('data', () => { await expectTypeOf(graffle.query.resultOrThrow({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | null>() }) test(`raw`, async () => { - expectTypeOf(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf() + expectTypeOf(graffle.raw({ document:'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() }) }) @@ -89,7 +89,7 @@ describe('successData', () => { }) }) test(`raw`, async () => { - expectTypeOf(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf() + expectTypeOf(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() }) }) @@ -112,7 +112,7 @@ describe('dataAndErrors', () => { await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | {__typename: "ErrorOne"} | {__typename: "ErrorTwo"} | null | GraphQLExecutionResultError>() }) test(`raw`, async () => { - expectTypeOf(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf() + expectTypeOf(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() }) }) @@ -141,7 +141,7 @@ describe('graphql', () => { await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf>() }) test(`raw`, async () => { - expectTypeOf(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf() + expectTypeOf(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() }) }) diff --git a/src/layers/5_client/client.returnMode.test.ts b/src/layers/5_client/client.returnMode.test.ts index 7fa04a0a5..125afa739 100644 --- a/src/layers/5_client/client.returnMode.test.ts +++ b/src/layers/5_client/client.returnMode.test.ts @@ -18,7 +18,7 @@ describe('default (data)', () => { await expect(graffle.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.errorAggregate) }) test('raw', async () => { - await expect(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({ data: { id: db.id } }) + await expect(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqual({ data: { id: db.id } }) }) test('query.', async () => { await expect(graffle.query.__typename()).resolves.toEqual('Query') @@ -56,7 +56,7 @@ describe('dataAndErrors', () => { await expect(graffle.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.errorAggregate) }) test('raw', async () => { - await expect(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({ data: { id: db.id } }) + await expect(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqual({ data: { id: db.id } }) }) test('query.', async () => { await expect(graffle.query.__typename()).resolves.toEqual('Query') @@ -131,7 +131,7 @@ describe('successData', () => { }) }) test(`raw`, async () => { - expect(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({data:{id:db.id}}) + expect(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqual({data:{id:db.id}}) }) }) @@ -148,7 +148,7 @@ describe('graphql', () => { await expect(graffle.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.errorAggregate) }) test('raw', async () => { - await expect(graffle.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({ data: { id: db.id } }) + await expect(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqual({ data: { id: db.id } }) }) test('query.', async () => { await expect(graffle.query.__typename()).resolves.toEqual({ data: { __typename: 'Query' } }) diff --git a/src/layers/5_client/client.ts b/src/layers/5_client/client.ts index aa4e30a27..2e7c97fd1 100644 --- a/src/layers/5_client/client.ts +++ b/src/layers/5_client/client.ts @@ -1,10 +1,11 @@ import type { ExecutionResult } from 'graphql' -import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql' -import request from '../../entrypoints/main.js' import { Errors } from '../../lib/errors/__.js' import type { SomeExecutionResultWithoutErrors } from '../../lib/graphql.js' -import { type RootTypeName, rootTypeNameToOperationName, type Variables } from '../../lib/graphql.js' +import { type RootTypeName, rootTypeNameToOperationName } from '../../lib/graphql.js' import { isPlainObject } from '../../lib/prelude.js' +import type { SchemaInput } from '../0_functions/requestOrExecute.js' +import { requestOrExecute } from '../0_functions/requestOrExecute.js' +import type { Input as RequestOrExecuteInput } from '../0_functions/requestOrExecute.js' import { Schema } from '../1_Schema/__.js' import { readMaybeThunk } from '../1_Schema/core/helpers.js' import type { GlobalRegistry } from '../2_generator/globalRegistry.js' @@ -22,6 +23,14 @@ import type { DocumentFn } from './document.js' import { toDocumentString } from './document.js' import type { GetRootTypeMethods } from './RootTypeMethods.js' +type RawInput = Omit + +// todo no config needed? +export type ClientRaw<_$Config extends Config> = { + raw: (input: RawInput) => Promise + rawOrThrow: (input: RawInput) => Promise +} + // dprint-ignore export type Client<$Index extends Schema.Index | null, $Config extends Config> = & ClientRaw<$Config> @@ -31,15 +40,6 @@ export type Client<$Index extends Schema.Index | null, $Config extends Config> = : {} // eslint-disable-line ) -type ClientRaw<_$Config extends Config> = { - raw: (document: string | DocumentNode, variables?: Variables, operationName?: string) => Promise - rawOrThrow: ( - document: string | DocumentNode, - variables?: Variables, - operationName?: string, - ) => Promise -} - export type ClientTyped<$Index extends Schema.Index, $Config extends Config> = & { document: DocumentFn<$Config, $Index> @@ -47,7 +47,7 @@ export type ClientTyped<$Index extends Schema.Index, $Config extends Config> = & GetRootTypeMethods<$Config, $Index> export type InputRaw = { - schema: URL | string | GraphQLSchema + schema: SchemaInput // todo condition on if schema is NOT GraphQLSchema headers?: HeadersInit } @@ -138,48 +138,6 @@ export const create: Create = ( */ const returnMode = input.returnMode ?? `data` as ReturnModeType - const executeGraphQLDocument = async ( - { document, variables, operationName }: { - document: string | DocumentNode - variables?: Variables - operationName?: string - }, - ): Promise => { - let result: ExecutionResult - if (input.schema instanceof URL || typeof input.schema === `string`) { - // todo return errors too - const data: ExecutionResult['data'] = await request({ - url: new URL(input.schema).href, // todo allow relative urls - what does fetch in node do? - requestHeaders: input.headers, - document, - variables, - // todo use operationName - }) - result = { data } - } else { - if (typeof document === `string`) { - result = await graphql({ - schema: input.schema, - source: document, - // contextValue: createContextValue(), // todo - variableValues: variables, - operationName, - }) - } else if (typeof document === `object`) { - result = await execute({ - schema: input.schema, - document, - // contextValue: createContextValue(), // todo - variableValues: variables, - operationName, - }) - } else { - throw new Error(`Unsupported GraphQL document type: ${String(document)}`) - } - } - return result - } - const executeRootType = (context: Context, rootTypeName: RootTypeName) => async (selection: GraphQLObjectSelection): Promise => { @@ -194,7 +152,7 @@ export const create: Create = ( selection[rootTypeNameToOperationName[rootTypeName]], ) // todo variables - const result = await executeGraphQLDocument({ document: documentString }) + const result = await requestOrExecute({ schema: input.schema, document: documentString }) // todo optimize // 1. Generate a map of possible custom scalar paths (tree structure) // 2. When traversing the result, skip keys that are not in the map @@ -281,11 +239,19 @@ export const create: Create = ( // @ts-expect-error ignoreme const client: Client = { - raw: async (document: string | DocumentNode, variables?: Variables, operationName?: string) => { - return await executeGraphQLDocument({ document, variables, operationName }) + raw: async (input2: RawInput) => { + return await requestOrExecute({ + ...input2, + schema: input.schema, + }) }, - rawOrThrow: async (document: string | DocumentNode, variables?: Variables, operationName?: string) => { - const result = await client.raw(document, variables, operationName) as ExecutionResult // eslint-disable-line + rawOrThrow: async ( + input2: RawInput, + ) => { + const result = await requestOrExecute({ + ...input2, + schema: input.schema, + }) // todo consolidate if (result.errors && result.errors.length > 0) { throw new Errors.ContextualAggregateError( @@ -327,13 +293,15 @@ export const create: Create = ( // todo this does not support custom scalars const documentString = toDocumentString(context, documentObject) - const result = await executeGraphQLDocument({ + const result = await requestOrExecute({ + schema: input.schema, document: documentString, operationName, // todo variables }) return handleReturn(schemaIndex, result, returnMode) } + return { run, runOrThrow: async (operationName: string) => { @@ -344,7 +312,8 @@ export const create: Create = ( returnMode: `successData`, }, }, documentObject) - const result = await executeGraphQLDocument({ + const result = await requestOrExecute({ + schema: input.schema, document: documentString, operationName, // todo variables @@ -365,7 +334,11 @@ export const create: Create = ( return client } -const handleReturn = (schemaIndex: Schema.Index, result: ExecutionResult, returnMode: ReturnModeType) => { +const handleReturn = ( + schemaIndex: Schema.Index, + result: ExecutionResult, + returnMode: ReturnModeType, +) => { switch (returnMode) { case `dataAndErrors`: case `successData`: diff --git a/src/layers/5_client/types.ts b/src/layers/5_client/types.ts new file mode 100644 index 000000000..b0ac627df --- /dev/null +++ b/src/layers/5_client/types.ts @@ -0,0 +1,5 @@ +import type { DocumentNode } from 'graphql' + +export type DocumentInput = DocumentNode | string + +export type OperationNameInput = string diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts index b6ff397aa..66759e24a 100644 --- a/src/lib/graphql.ts +++ b/src/lib/graphql.ts @@ -243,7 +243,9 @@ export const hasMutation = (typeMapByKind: TypeMapByKind) => export const hasSubscription = (typeMapByKind: TypeMapByKind) => typeMapByKind.GraphQLRootType.find((_) => _.name === `Subscription`) -export type Variables = Record // todo or any custom scalars too +export type StandardScalarVariables = { + [key: string]: string | boolean | null | number | StandardScalarVariables +} export type GraphQLExecutionResultError = Errors.ContextualAggregateError diff --git a/src/lib/graphqlHTTP.ts b/src/lib/graphqlHTTP.ts new file mode 100644 index 000000000..e301319dd --- /dev/null +++ b/src/lib/graphqlHTTP.ts @@ -0,0 +1,46 @@ +import type { GraphQLFormattedError } from 'graphql' +import { type ExecutionResult, GraphQLError } from 'graphql' +import { isPlainObject } from './prelude.js' + +export const parseExecutionResult = (result: unknown): ExecutionResult => { + if (typeof result !== `object` || result === null) { + throw new Error(`Invalid execution result: result is not object`) + } + + let errors = undefined + let data = undefined + let extensions = undefined + + if (`errors` in result) { + if ( + !Array.isArray(result.errors) + || result.errors.some( + error => !(isPlainObject(error) && `message` in error && typeof error[`message`] === `string`), + ) + ) { + throw new Error(`Invalid execution result: errors is not array of formatted errors`) // prettier-ignore + } + errors = result.errors.map((error: GraphQLFormattedError) => + error instanceof GraphQLError ? error : new GraphQLError(error.message, error) + ) + } + + // todo add test coverage for case of null. @see https://github.com/jasonkuhrt/graphql-request/issues/739 + if (`data` in result) { + if (!isPlainObject(result.data) && result.data !== null) { + throw new Error(`Invalid execution result: data is not plain object`) // prettier-ignore + } + data = result.data + } + + if (`extensions` in result) { + if (!isPlainObject(result.extensions)) throw new Error(`Invalid execution result: extensions is not plain object`) // prettier-ignore + extensions = result.extensions + } + + return { + data, + errors, + extensions, + } +} diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts new file mode 100644 index 000000000..e4df0b67e --- /dev/null +++ b/tests/_/helpers.ts @@ -0,0 +1,23 @@ +import type { Mock } from 'vitest' +import { test as testBase, vi } from 'vitest' +import { CONTENT_TYPE_JSON } from '../../src/lib/http.js' + +export const createResponse = (body: object) => + new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': CONTENT_TYPE_JSON } }) + +interface Fixtures { + fetch: Mock +} + +export const test = testBase.extend({ + // @ts-expect-error https://github.com/vitest-dev/vitest/discussions/5710 + // eslint-disable-next-line + fetch: async ({}, use) => { + const fetch = globalThis.fetch + const fetchMock = vi.fn() + globalThis.fetch = fetchMock + // eslint-disable-next-line + await use(fetchMock) + globalThis.fetch = fetch + }, +}) diff --git a/vite.config.ts b/vite.config.ts index 83ff5abd8..046604e2e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,9 +4,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ resolve: { alias: { - 'graphql/language/ast.js': 'graphql/language/ast.js', - 'graphql/language/parser.js': 'graphql/language/parser.js', - 'graphql/language/printer.js': 'graphql/language/printer.js', graphql: 'graphql/index.js', }, },