diff --git a/package.json b/package.json index b0e983252..566869256 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "@molt/command": "^0.9.0", + "is-plain-obj": "^4.1.0", "zod": "^3.23.8" }, "peerDependencies": { @@ -103,21 +104,21 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", - "@pothos/core": "^3.41.1", - "@pothos/plugin-simple-objects": "^3.7.0", + "@pothos/core": "^3.41.2", + "@pothos/plugin-simple-objects": "^3.7.1", "@tsconfig/node18": "^18.2.4", "@tsconfig/strictest": "^2.0.5", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", "@types/json-bigint": "^1.0.4", - "@types/node": "^20.14.7", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", + "@types/node": "^20.14.9", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", "apollo-server-express": "^3.13.0", "body-parser": "^1.20.2", "doctoc": "^2.2.1", "dripip": "^0.10.0", - "eslint": "^9.5.0", + "eslint": "^9.6.0", "eslint-config-prisma": "^0.6.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-only-warn": "^1.1.0", @@ -130,13 +131,15 @@ "graphql": "^16.9.0", "graphql-scalars": "^1.23.0", "graphql-tag": "^2.12.6", + "graphql-upload-minimal": "^1.6.1", + "graphql-yoga": "^5.5.0", "jsdom": "^24.1.0", "json-bigint": "^1.0.0", "publint": "^0.2.8", - "tsx": "^4.15.7", + "tsx": "^4.16.0", "type-fest": "^4.20.1", "typescript": "^5.5.2", - "typescript-eslint": "^7.13.1", + "typescript-eslint": "^7.14.1", "vitest": "^1.6.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a374f8d65..7cc8fc50f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: dprint: specifier: ^0.46.2 version: 0.46.2 + is-plain-obj: + specifier: ^4.1.0 + version: 4.1.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -31,10 +34,10 @@ importers: specifier: ^0.15.3 version: 0.15.3 '@pothos/core': - specifier: ^3.41.1 + specifier: ^3.41.2 version: 3.41.2(graphql@16.9.0) '@pothos/plugin-simple-objects': - specifier: ^3.7.0 + specifier: ^3.7.1 version: 3.7.1(@pothos/core@3.41.2(graphql@16.9.0))(graphql@16.9.0) '@tsconfig/node18': specifier: ^18.2.4 @@ -52,13 +55,13 @@ importers: specifier: ^1.0.4 version: 1.0.4 '@types/node': - specifier: ^20.14.7 - version: 20.14.7 + specifier: ^20.14.9 + version: 20.14.9 '@typescript-eslint/eslint-plugin': - specifier: ^7.13.1 + specifier: ^7.14.1 version: 7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2) '@typescript-eslint/parser': - specifier: ^7.13.1 + specifier: ^7.14.1 version: 7.14.1(eslint@9.6.0)(typescript@5.5.2) apollo-server-express: specifier: ^3.13.0 @@ -73,7 +76,7 @@ importers: specifier: ^0.10.0 version: 0.10.0 eslint: - specifier: ^9.5.0 + specifier: ^9.6.0 version: 9.6.0 eslint-config-prisma: specifier: ^0.6.0 @@ -111,6 +114,12 @@ importers: graphql-tag: specifier: ^2.12.6 version: 2.12.6(graphql@16.9.0) + graphql-upload-minimal: + specifier: ^1.6.1 + version: 1.6.1(graphql@16.9.0) + graphql-yoga: + specifier: ^5.5.0 + version: 5.5.0(graphql@16.9.0) jsdom: specifier: ^24.1.0 version: 24.1.0 @@ -121,8 +130,8 @@ importers: specifier: ^0.2.8 version: 0.2.8 tsx: - specifier: ^4.15.7 - version: 4.15.8 + specifier: ^4.16.0 + version: 4.16.0 type-fest: specifier: ^4.20.1 version: 4.20.1 @@ -130,11 +139,11 @@ importers: specifier: ^5.5.2 version: 5.5.2 typescript-eslint: - specifier: ^7.13.1 + specifier: ^7.14.1 version: 7.14.1(eslint@9.6.0)(typescript@5.5.2) vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.14.7)(happy-dom@14.12.3)(jsdom@24.1.0) + version: 1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(jsdom@24.1.0) packages: @@ -257,6 +266,14 @@ packages: cpu: [x64] os: [win32] + '@envelop/core@5.0.1': + resolution: {integrity: sha512-wxA8EyE1fPnlbP0nC/SFI7uU8wSNf4YjxZhAPu0P63QbgIvqHtHsH4L3/u+rsTruzhk3OvNRgQyLsMfaR9uzAQ==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.0.0': + resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} + engines: {node: '>=18.0.0'} + '@es-joy/jsdoccomment@0.4.4': resolution: {integrity: sha512-ua4qDt9dQb4qt5OI38eCZcQZYE5Bq3P0GzgvDARdT8Lt0mAUpxKTPy8JGGqEvF77tG1irKDZ3WreeezEa3P43w==} engines: {node: '>=10.0.0'} @@ -405,10 +422,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.10.1': - resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.11.0': resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -421,10 +434,6 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.5.0': - resolution: {integrity: sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.6.0': resolution: {integrity: sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -433,6 +442,12 @@ packages: resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@graphql-tools/executor@1.2.7': + resolution: {integrity: sha512-oyIw69QA+PuS/g7ttZZeEpIPS5CCGiIYitGtNxaChuiK7NPb7FD1dwOEXyekQt9/2FOEqZoYNpRY0NFfx/tO9Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/merge@8.3.1': resolution: {integrity: sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==} peerDependencies: @@ -443,11 +458,23 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/merge@9.0.4': + resolution: {integrity: sha512-MivbDLUQ+4Q8G/Hp/9V72hbn810IJDEZQ57F01sHnlrrijyadibfVhaQfW/pNH+9T/l8ySZpaR/DpL5i+ruZ+g==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/mock@8.7.20': resolution: {integrity: sha512-ljcHSJWjC/ZyzpXd5cfNhPI7YljRVvabKHPzKjEs5ElxWu2cdlLGvyNYepApXDsM/OJG/2xuhGM+9GWu5gEAPQ==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/schema@10.0.4': + resolution: {integrity: sha512-HuIwqbKxPaJujox25Ra4qwz0uQzlpsaBOzO6CVfzB/MemZdd+Gib8AIvfhQArK0YIN40aDran/yi+E5Xf0mQww==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/schema@8.5.1': resolution: {integrity: sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==} peerDependencies: @@ -458,6 +485,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@10.2.2': + resolution: {integrity: sha512-ueoplzHIgFfxhFrF4Mf/niU/tYHuO6Uekm2nCYU72qpI+7Hn9dA2/o5XOBvFXDk27Lp5VSvQY5WfmRbqwVxaYQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@8.9.0': resolution: {integrity: sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==} peerDependencies: @@ -473,6 +506,18 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-yoga/logger@2.0.0': + resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/subscription@5.0.1': + resolution: {integrity: sha512-1wCB1DfAnaLzS+IdoOzELGGnx1ODEg9nzQXFh4u2j02vAnne6d+v4A7HIH9EqzVdPLoAaMKXCZUUdKs+j3z1fg==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/typed-event-target@3.0.0': + resolution: {integrity: sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==} + engines: {node: '>=18.0.0'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -491,6 +536,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@kamilkisiela/fast-url-parser@1.1.4': + resolution: {integrity: sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -653,6 +701,9 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@repeaterjs/repeater@3.0.6': + resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@rollup/rollup-android-arm-eabi@4.18.0': resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} cpu: [arm] @@ -821,8 +872,8 @@ packages: '@types/node@10.17.60': resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} - '@types/node@20.14.7': - resolution: {integrity: sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==} + '@types/node@20.14.9': + resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==} '@types/parse-git-config@3.0.4': resolution: {integrity: sha512-jz5eGdk9lBgAd4rMbXTP7MRG7AsGQ8DrXsRumDcXDLClHcpKluislylPVMP/qp90J/LlIrrPZRZIQUflHfrDnQ==} @@ -897,10 +948,6 @@ packages: resolution: {integrity: sha512-5RFjdA/ain/MDUHYXdF173btOKncIrLuBmA9s6FJhzDrRAyVSA+70BHg0/MW6TE+UiKVyRtX91XpVS0gVNwVDQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/scope-manager@7.13.1': - resolution: {integrity: sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/scope-manager@7.14.1': resolution: {integrity: sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -919,10 +966,6 @@ packages: resolution: {integrity: sha512-dU/pKBUpehdEqYuvkojmlv0FtHuZnLXFBn16zsDmlFF3LXkOpkAQ2vrKc3BidIIve9EMH2zfTlxqw9XM0fFN5w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/types@7.13.1': - resolution: {integrity: sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/types@7.14.1': resolution: {integrity: sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -936,15 +979,6 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@7.13.1': - resolution: {integrity: sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@7.14.1': resolution: {integrity: sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -954,12 +988,6 @@ packages: typescript: optional: true - '@typescript-eslint/utils@7.13.1': - resolution: {integrity: sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - '@typescript-eslint/utils@7.14.1': resolution: {integrity: sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==} engines: {node: ^18.18.0 || >=20.0.0} @@ -970,10 +998,6 @@ packages: resolution: {integrity: sha512-yRyd2++o/IrJdyHuYMxyFyBhU762MRHQ/bAGQeTnN3pGikfh+nEmM61XTqaDH1XDp53afZ+waXrk0ZvenoZ6xw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/visitor-keys@7.13.1': - resolution: {integrity: sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/visitor-keys@7.14.1': resolution: {integrity: sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -993,6 +1017,22 @@ packages: '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@whatwg-node/events@0.1.1': + resolution: {integrity: sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/fetch@0.9.18': + resolution: {integrity: sha512-hqoz6StCW+AjV/3N+vg0s1ah82ptdVUb9nH2ttj3UbySOXUvytWw2yqy8c1cKzyRk6mDD00G47qS3fZI9/gMjg==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/node-fetch@0.5.11': + resolution: {integrity: sha512-LS8tSomZa3YHnntpWt3PP43iFEEl6YeIsvDakczHBKlay5LdkXFr8w7v8H6akpG5nRrzydyB0k1iE2eoL6aKIQ==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/server@0.9.36': + resolution: {integrity: sha512-KT9qKLmbuWSuFv0Vg4JyK2vN2+vSuQPeEa25xpndYFROAIZntYe7e2BlWAk9l7IrgnV+M4bCVhjrAwwRsaCeiA==} + engines: {node: '>=16.0.0'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1182,6 +1222,10 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1306,6 +1350,10 @@ packages: engines: {node: '>=0.8'} hasBin: true + cross-inspect@1.0.0: + resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} + engines: {node: '>=16.0.0'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1439,6 +1487,10 @@ packages: resolution: {integrity: sha512-FaHkW6uXAa57pJwz+SRxTvTDiybSH9w4PGWXkheoIPNs4HcHM688rfsKzvedoaLvQul4UaAoRr+2CHc7V25biA==} hasBin: true + dset@3.1.3: + resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} + engines: {node: '>=4'} + ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} @@ -1743,6 +1795,9 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1759,6 +1814,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1932,6 +1990,18 @@ packages: peerDependencies: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-upload-minimal@1.6.1: + resolution: {integrity: sha512-wNUf/KqA0B/OguL1k6qWa4AmAduLUAhXzovh9i14SKbpBa8HX2vc7f+fR67S0rG7fSpGdM/aivxzC329/+9xXw==} + engines: {node: '>=12'} + peerDependencies: + graphql: 0.13.1 - 16 + + graphql-yoga@5.5.0: + resolution: {integrity: sha512-GrF1GfR5RJb/iHEcRQakXSSoqXb2dgGa0UCU0CXsfYnuUstSog6nF5oCUKlXz1UnFbpNAlVhU++rd088S4VAtg==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + graphql@16.9.0: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -2001,8 +2071,8 @@ packages: 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==} + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} human-signals@5.0.0: @@ -2138,6 +2208,10 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2185,8 +2259,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-git@1.25.10: - resolution: {integrity: sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==} + isomorphic-git@1.26.3: + resolution: {integrity: sha512-NVicJz3RvUhllSSZnCVtTOqWhxlMln5OD3mh00334wCYhiMDuMiYsNJqs3sCHL7oXiv1tP93jMaQTdN7DkPCOg==} engines: {node: '>=12'} hasBin: true @@ -2309,6 +2383,10 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lru-cache@10.3.0: + resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==} + engines: {node: 14 || >=16.14} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -2528,8 +2606,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -2596,9 +2675,9 @@ packages: resolution: {integrity: sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==} engines: {node: '>=8'} - parse-github-url@1.0.2: - resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==} - engines: {node: '>=0.10.0'} + parse-github-url@1.0.3: + resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==} + engines: {node: '>= 0.10'} hasBin: true parse-json@4.0.0: @@ -2971,6 +3050,10 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-length@6.0.0: resolution: {integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==} engines: {node: '>=16'} @@ -3102,8 +3185,8 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsx@4.15.8: - resolution: {integrity: sha512-B8dMlbbkZPW0GQ7wafyy2TGXyoGYW0IURfWkM1h/WzgG5lxxRoeDU2VbMURmmjwGaCsoKROVTLmQQPe/s2TnLw==} + tsx@4.16.0: + resolution: {integrity: sha512-MPgN+CuY+4iKxGoJNPv+1pyo5YWZAQ5XfsyobUG+zoKG7IkvCPLZDEyoIb8yLS2FcWci1nlxAqmvPlFWD5AFiQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -3226,6 +3309,9 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3276,8 +3362,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.3.1: - resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} + vite@5.3.2: + resolution: {integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3426,8 +3512,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + yocto-queue@1.1.0: + resolution: {integrity: sha512-cMojmlnwkAgIXqga+2sXshlgrrcI0QEPJ5n58pEvtuFo4PaekfomlCudArDD7hj8Hkswjl0/x4eu4q+Xa0WFgQ==} engines: {node: '>=12.20'} zod@3.23.8: @@ -3568,6 +3654,15 @@ snapshots: '@dprint/win32-x64@0.46.2': optional: true + '@envelop/core@5.0.1': + dependencies: + '@envelop/types': 5.0.0 + tslib: 2.6.3 + + '@envelop/types@5.0.0': + dependencies: + tslib: 2.6.3 + '@es-joy/jsdoccomment@0.4.4': dependencies: comment-parser: 1.1.5 @@ -3648,8 +3743,6 @@ snapshots: eslint: 9.6.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.10.1': {} - '@eslint-community/regexpp@4.11.0': {} '@eslint/config-array@0.17.0': @@ -3674,12 +3767,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.5.0': {} - '@eslint/js@9.6.0': {} '@eslint/object-schema@2.1.4': {} + '@graphql-tools/executor@1.2.7(graphql@16.9.0)': + dependencies: + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) + '@repeaterjs/repeater': 3.0.6 + graphql: 16.9.0 + tslib: 2.6.3 + value-or-promise: 1.0.12 + '@graphql-tools/merge@8.3.1(graphql@16.9.0)': dependencies: '@graphql-tools/utils': 8.9.0(graphql@16.9.0) @@ -3692,6 +3792,12 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 + '@graphql-tools/merge@9.0.4(graphql@16.9.0)': + dependencies: + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + graphql: 16.9.0 + tslib: 2.6.3 + '@graphql-tools/mock@8.7.20(graphql@16.9.0)': dependencies: '@graphql-tools/schema': 9.0.19(graphql@16.9.0) @@ -3700,6 +3806,14 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 + '@graphql-tools/schema@10.0.4(graphql@16.9.0)': + dependencies: + '@graphql-tools/merge': 9.0.4(graphql@16.9.0) + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + graphql: 16.9.0 + tslib: 2.6.3 + value-or-promise: 1.0.12 + '@graphql-tools/schema@8.5.1(graphql@16.9.0)': dependencies: '@graphql-tools/merge': 8.3.1(graphql@16.9.0) @@ -3716,6 +3830,14 @@ snapshots: tslib: 2.6.3 value-or-promise: 1.0.12 + '@graphql-tools/utils@10.2.2(graphql@16.9.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) + cross-inspect: 1.0.0 + dset: 3.1.3 + graphql: 16.9.0 + tslib: 2.6.3 + '@graphql-tools/utils@8.9.0(graphql@16.9.0)': dependencies: graphql: 16.9.0 @@ -3731,6 +3853,22 @@ snapshots: dependencies: graphql: 16.9.0 + '@graphql-yoga/logger@2.0.0': + dependencies: + tslib: 2.6.3 + + '@graphql-yoga/subscription@5.0.1': + dependencies: + '@graphql-yoga/typed-event-target': 3.0.0 + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/events': 0.1.1 + tslib: 2.6.3 + + '@graphql-yoga/typed-event-target@3.0.0': + dependencies: + '@repeaterjs/repeater': 3.0.6 + tslib: 2.6.3 + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} @@ -3743,6 +3881,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@kamilkisiela/fast-url-parser@1.1.4': {} + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.3.5 @@ -4009,6 +4149,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@repeaterjs/repeater@3.0.6': {} + '@rollup/rollup-android-arm-eabi@4.18.0': optional: true @@ -4083,21 +4225,21 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/body-parser@1.19.2': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/connect@3.4.38': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/cors@2.8.12': {} @@ -4120,13 +4262,13 @@ snapshots: '@types/express-serve-static-core@4.17.31': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -4165,7 +4307,7 @@ snapshots: '@types/node@10.17.60': {} - '@types/node@20.14.7': + '@types/node@20.14.9': dependencies: undici-types: 5.26.5 @@ -4173,7 +4315,7 @@ snapshots: '@types/parse-github-url@1.0.3': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/qs@6.9.15': {} @@ -4182,12 +4324,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/send': 0.17.4 '@types/unist@2.0.10': {} @@ -4211,7 +4353,7 @@ snapshots: '@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2)': dependencies: - '@eslint-community/regexpp': 4.10.1 + '@eslint-community/regexpp': 4.11.0 '@typescript-eslint/parser': 7.14.1(eslint@9.6.0)(typescript@5.5.2) '@typescript-eslint/scope-manager': 7.14.1 '@typescript-eslint/type-utils': 7.14.1(eslint@9.6.0)(typescript@5.5.2) @@ -4270,11 +4412,6 @@ snapshots: '@typescript-eslint/types': 5.0.0 '@typescript-eslint/visitor-keys': 5.0.0 - '@typescript-eslint/scope-manager@7.13.1': - dependencies: - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/visitor-keys': 7.13.1 - '@typescript-eslint/scope-manager@7.14.1': dependencies: '@typescript-eslint/types': 7.14.1 @@ -4294,8 +4431,6 @@ snapshots: '@typescript-eslint/types@5.0.0': {} - '@typescript-eslint/types@7.13.1': {} - '@typescript-eslint/types@7.14.1': {} '@typescript-eslint/typescript-estree@5.0.0(typescript@4.4.3)': @@ -4312,21 +4447,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.13.1(typescript@5.5.2)': - dependencies: - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/visitor-keys': 7.13.1 - debug: 4.3.5 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.2 - ts-api-utils: 1.3.0(typescript@5.5.2) - optionalDependencies: - typescript: 5.5.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@7.14.1(typescript@5.5.2)': dependencies: '@typescript-eslint/types': 7.14.1 @@ -4342,17 +4462,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.13.1(eslint@9.6.0)(typescript@5.5.2)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) - '@typescript-eslint/scope-manager': 7.13.1 - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.5.2) - eslint: 9.6.0 - transitivePeerDependencies: - - supports-color - - typescript - '@typescript-eslint/utils@7.14.1(eslint@9.6.0)(typescript@5.5.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) @@ -4369,11 +4478,6 @@ snapshots: '@typescript-eslint/types': 5.0.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@7.13.1': - dependencies: - '@typescript-eslint/types': 7.13.1 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@7.14.1': dependencies: '@typescript-eslint/types': 7.14.1 @@ -4408,6 +4512,26 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@whatwg-node/events@0.1.1': {} + + '@whatwg-node/fetch@0.9.18': + dependencies: + '@whatwg-node/node-fetch': 0.5.11 + urlpattern-polyfill: 10.0.0 + + '@whatwg-node/node-fetch@0.5.11': + dependencies: + '@kamilkisiela/fast-url-parser': 1.1.4 + '@whatwg-node/events': 0.1.1 + busboy: 1.6.0 + fast-querystring: 1.1.2 + tslib: 2.6.3 + + '@whatwg-node/server@0.9.36': + dependencies: + '@whatwg-node/fetch': 0.9.18 + tslib: 2.6.3 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -4658,6 +4782,10 @@ snapshots: dependencies: fill-range: 7.1.1 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -4763,6 +4891,10 @@ snapshots: crc-32@1.2.2: {} + cross-inspect@1.0.0: + dependencies: + tslib: 2.6.3 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4911,15 +5043,17 @@ snapshots: common-tags: 1.8.2 debug: 4.3.5 fs-jetpack: 3.2.0 - isomorphic-git: 1.25.10 + isomorphic-git: 1.26.3 parse-git-config: 3.0.0 - parse-github-url: 1.0.2 + parse-github-url: 1.0.3 request: 2.88.2 simple-git: 2.48.0 transitivePeerDependencies: - encoding - supports-color + dset@3.1.3: {} + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 @@ -4978,7 +5112,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.13 is-weakref: 1.0.2 - object-inspect: 1.13.1 + object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.2 @@ -5061,12 +5195,12 @@ snapshots: eslint-config-prisma@0.6.0(@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2))(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint-plugin-deprecation@3.0.0(eslint@9.6.0)(typescript@5.5.2))(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3(eslint@9.6.0))(eslint-plugin-tsdoc@0.3.0)(eslint@9.6.0)(typescript@5.5.2): dependencies: - '@eslint/js': 9.5.0 + '@eslint/js': 9.6.0 '@types/eslint-config-prettier': 6.11.3 '@types/eslint__js': 8.42.3 '@typescript-eslint/eslint-plugin': 7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2) '@typescript-eslint/parser': 7.14.1(eslint@9.6.0)(typescript@5.5.2) - '@typescript-eslint/utils': 7.13.1(eslint@9.6.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.14.1(eslint@9.6.0)(typescript@5.5.2) eslint: 9.6.0 eslint-config-prettier: 9.1.0(eslint@9.6.0) eslint-plugin-deprecation: 3.0.0(eslint@9.6.0)(typescript@5.5.2) @@ -5118,7 +5252,7 @@ snapshots: eslint-plugin-deprecation@3.0.0(eslint@9.6.0)(typescript@5.5.2): dependencies: - '@typescript-eslint/utils': 7.13.1(eslint@9.6.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.14.1(eslint@9.6.0)(typescript@5.5.2) eslint: 9.6.0 ts-api-utils: 1.3.0(typescript@5.5.2) tslib: 2.6.3 @@ -5382,6 +5516,8 @@ snapshots: extsprintf@1.3.0: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -5398,6 +5534,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -5588,6 +5728,26 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 + graphql-upload-minimal@1.6.1(graphql@16.9.0): + dependencies: + busboy: 1.6.0 + graphql: 16.9.0 + + graphql-yoga@5.5.0(graphql@16.9.0): + dependencies: + '@envelop/core': 5.0.1 + '@graphql-tools/executor': 1.2.7(graphql@16.9.0) + '@graphql-tools/schema': 10.0.4(graphql@16.9.0) + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + '@graphql-yoga/logger': 2.0.0 + '@graphql-yoga/subscription': 5.0.1 + '@whatwg-node/fetch': 0.9.18 + '@whatwg-node/server': 0.9.36 + dset: 3.1.3 + graphql: 16.9.0 + lru-cache: 10.3.0 + tslib: 2.6.3 + graphql@16.9.0: {} happy-dom@14.12.3: @@ -5660,7 +5820,7 @@ snapshots: jsprim: 1.4.2 sshpk: 1.18.0 - https-proxy-agent@7.0.4: + https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 debug: 4.3.5 @@ -5774,6 +5934,8 @@ snapshots: is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -5815,7 +5977,7 @@ snapshots: isexe@2.0.0: {} - isomorphic-git@1.25.10: + isomorphic-git@1.26.3: dependencies: async-lock: 1.4.1 clean-git-ref: 2.0.1 @@ -5851,7 +6013,7 @@ snapshots: form-data: 4.0.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 + https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.10 parse5: 7.1.2 @@ -5956,6 +6118,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + lru-cache@10.3.0: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -6214,7 +6378,7 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.1: {} + object-inspect@1.13.2: {} object-keys@1.1.1: {} @@ -6262,7 +6426,7 @@ snapshots: p-limit@5.0.0: dependencies: - yocto-queue: 1.0.0 + yocto-queue: 1.1.0 p-locate@2.0.0: dependencies: @@ -6294,7 +6458,7 @@ snapshots: git-config-path: 2.0.0 ini: 1.3.8 - parse-github-url@1.0.2: {} + parse-github-url@1.0.3: {} parse-json@4.0.0: dependencies: @@ -6641,7 +6805,7 @@ snapshots: call-bind: 1.0.7 es-errors: 1.3.0 get-intrinsic: 1.2.4 - object-inspect: 1.13.1 + object-inspect: 1.13.2 siginfo@2.0.0: {} @@ -6703,6 +6867,8 @@ snapshots: std-env@3.7.0: {} + streamsearch@1.1.0: {} + string-length@6.0.0: dependencies: strip-ansi: 7.1.0 @@ -6831,7 +6997,7 @@ snapshots: tslib: 1.14.1 typescript: 4.4.3 - tsx@4.15.8: + tsx@4.16.0: dependencies: esbuild: 0.21.5 get-tsconfig: 4.7.5 @@ -6970,6 +7136,8 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + urlpattern-polyfill@10.0.0: {} + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -7009,13 +7177,13 @@ snapshots: unist-util-stringify-position: 2.0.3 vfile-message: 2.0.4 - vite-node@1.6.0(@types/node@20.14.7): + vite-node@1.6.0(@types/node@20.14.9): dependencies: cac: 6.7.14 debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.3.1(@types/node@20.14.7) + vite: 5.3.2(@types/node@20.14.9) transitivePeerDependencies: - '@types/node' - less @@ -7026,16 +7194,16 @@ snapshots: - supports-color - terser - vite@5.3.1(@types/node@20.14.7): + vite@5.3.2(@types/node@20.14.9): dependencies: esbuild: 0.21.5 postcss: 8.4.38 rollup: 4.18.0 optionalDependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.14.7)(happy-dom@14.12.3)(jsdom@24.1.0): + vitest@1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(jsdom@24.1.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -7054,11 +7222,11 @@ snapshots: strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.3.1(@types/node@20.14.7) - vite-node: 1.6.0(@types/node@20.14.7) + vite: 5.3.2(@types/node@20.14.9) + vite-node: 1.6.0(@types/node@20.14.9) why-is-node-running: 2.2.2 optionalDependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 happy-dom: 14.12.3 jsdom: 24.1.0 transitivePeerDependencies: @@ -7156,7 +7324,7 @@ snapshots: yocto-queue@0.1.0: {} - yocto-queue@1.0.0: {} + yocto-queue@1.1.0: {} zod@3.23.8: {} diff --git a/src/layers/3_SelectionSet/types.test-d.ts b/src/layers/3_SelectionSet/types.test-d.ts index dd670977d..2f13727a5 100644 --- a/src/layers/3_SelectionSet/types.test-d.ts +++ b/src/layers/3_SelectionSet/types.test-d.ts @@ -48,7 +48,7 @@ test(`Query`, () => { assertType({ objectNonNull: { id: true } }) // @ts-expect-error excess property check assertType({ id2: true }) - // @ts-expect-error excess property check + // @ts-expect-error no a2 assertType({ object: { a2: true } }) // Union diff --git a/src/layers/5_client/client.document.test.ts b/src/layers/5_client/client.document.test.ts index bed8bea6d..461a2d249 100644 --- a/src/layers/5_client/client.document.test.ts +++ b/src/layers/5_client/client.document.test.ts @@ -14,15 +14,17 @@ describe(`document with two queries`, () => { }) // todo allow sync extensions - // eslint-disable-next-line - graffle.extend(async ({ exchange }) => { + + graffle.use(async ({ exchange }) => { if (exchange.input.transport !== `http`) return exchange() // @ts-expect-error Nextjs exchange.input.request.next = { revalidate: 60, tags: [`menu`] } return exchange({ - ...exchange.input, - request: { - ...exchange.input.request, + input: { + ...exchange.input, + request: { + ...exchange.input.request, + }, }, }) }).document({ diff --git a/src/layers/5_client/client.extend.test.ts b/src/layers/5_client/client.extend.test.ts index 41dec82de..0b10d8e43 100644 --- a/src/layers/5_client/client.extend.test.ts +++ b/src/layers/5_client/client.extend.test.ts @@ -16,13 +16,13 @@ describe(`entrypoint request`, () => { expect(input.headers.get('x-foo')).toEqual(headers['x-foo']) return createResponse({ data: { id: db.id } }) }) - const client2 = client.extend(async ({ pack }) => { + const client2 = client.use(async ({ pack }) => { // todo should be raw input types but rather resolved // todo should be URL instance? // todo these input type tests should be moved down to Anyware // expectTypeOf(exchange).toEqualTypeOf() // expect(exchange.input).toEqual({ url: 'https://foo', document: `query { id \n }` }) - return await pack({ ...pack.input, headers }) + return await pack({ input: { ...pack.input, headers } }) }) expect(await client2.query.id()).toEqual(db.id) }) @@ -30,9 +30,9 @@ describe(`entrypoint request`, () => { fetch.mockImplementationOnce(async () => { return createResponse({ data: { id: db.id } }) }) - const client2 = client.extend(async ({ pack }) => { - const { exchange } = await pack({ ...pack.input, headers }) - return await exchange(exchange.input) + const client2 = client.use(async ({ pack }) => { + const { exchange } = await pack({ input: { ...pack.input, headers } }) + return await exchange({ input: exchange.input }) }) expect(await client2.query.id()).toEqual(db.id) }) diff --git a/src/layers/5_client/client.ts b/src/layers/5_client/client.ts index 6ba7ca985..64aff7b45 100644 --- a/src/layers/5_client/client.ts +++ b/src/layers/5_client/client.ts @@ -11,7 +11,7 @@ import { readMaybeThunk } from '../1_Schema/core/helpers.js' import type { GlobalRegistry } from '../2_generator/globalRegistry.js' import type { DocumentObject, GraphQLObjectSelection } from '../3_SelectionSet/encode.js' import { Core } from '../5_core/__.js' -import { type HookInputEncode } from '../5_core/core.js' +import { type HookDefEncode } from '../5_core/core.js' import type { InterfaceRaw } from '../5_core/types.js' import type { ApplyInputDefaults, @@ -40,7 +40,7 @@ export type SelectionSetOrArgs = object export interface Context { retry: undefined | Anyware.Extension2 - extensions: Anyware.Extension2[] + extensions: Extension[] config: Config } @@ -56,6 +56,11 @@ export type ClientRaw<_$Config extends Config> = { rawOrThrow: (input: Omit) => Promise } +export type Extension = { + name: string + anyware?: Anyware.Extension2 +} + // dprint-ignore export type Client<$Index extends Schema.Index | null, $Config extends Config> = & ClientRaw<$Config> @@ -65,7 +70,7 @@ export type Client<$Index extends Schema.Index | null, $Config extends Config> = : {} // eslint-disable-line ) & { - extend: (extension: Anyware.Extension2) => Client<$Index, $Config> + use: (extension: Extension | Anyware.Extension2) => Client<$Index, $Config> retry: (extension: Anyware.Extension2) => Client<$Index, $Config> } @@ -153,7 +158,7 @@ export const create: Create = ( interface CreateState { retry?: Anyware.Extension2 - extensions: Anyware.Extension2[] + extensions: Extension[] } export const createInternal = ( @@ -191,7 +196,7 @@ export const createInternal = ( interface: interface_, schemaIndex: context.schemaIndex, }, - } as HookInputEncode + } as HookDefEncode['input'] return await run(context, initialInput) } @@ -261,11 +266,11 @@ export const createInternal = ( }, } - const run = async (context: Context, initialInput: HookInputEncode) => { + const run = async (context: Context, initialInput: HookDefEncode['input']) => { const result = await Core.anyware.run({ initialInput, retryingExtension: context.retry, - extensions: context.extensions, + extensions: context.extensions.filter(_ => _.anyware !== undefined).map(_ => _.anyware!), }) as GraffleExecutionResult return handleReturn(context, result) } @@ -281,7 +286,8 @@ export const createInternal = ( context: { config: context.config, }, - } as HookInputEncode + variables: rawInput.variables, + } as HookDefEncode['input'] return await run(context, initialInput) } @@ -297,7 +303,10 @@ export const createInternal = ( const contextWithReturnModeSet = updateContextConfig(context, { returnMode: `graphqlSuccess` }) return await runRaw(contextWithReturnModeSet, rawInput) }, - extend: (extension: Anyware.Extension2) => { + use: (extensionOrAnyware: Extension | Anyware.Extension2) => { + const extension = typeof extensionOrAnyware === `function` + ? { anyware: extensionOrAnyware, name: extensionOrAnyware.name } + : extensionOrAnyware // todo test that adding extensions returns a copy of client return createInternal(input, { extensions: [...state.extensions, extension] }) }, diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index ca5e6a870..2465705fb 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -52,145 +52,194 @@ export const hookNamesOrderedBySequence = [`encode`, `pack`, `exchange`, `unpack export type HookSequence = typeof hookNamesOrderedBySequence -export type HookInputEncode = - & InterfaceInput<{ selection: GraphQLObjectSelection }, { document: string | DocumentNode }> - & TransportInput<{ schema: string | URL }, { schema: GraphQLSchema }> - -export type HookInputPack = - & { - document: string | DocumentNode - variables: StandardScalarVariables - operationName?: string +export type HookDefEncode = { + input: + & InterfaceInput< + { selection: GraphQLObjectSelection }, + { document: string | DocumentNode; variables?: StandardScalarVariables } + > + & TransportInput<{ schema: string | URL }, { schema: GraphQLSchema }> + slots: { + body: ( + input: { query: string; variables?: StandardScalarVariables; operationName?: string }, + ) => BodyInit } - & InterfaceInput - & TransportInput<{ url: string | URL; headers?: HeadersInit }, { schema: GraphQLSchema }> +} -export type ExchangeInputHook = - & InterfaceInput - & TransportInput< - { request: Request }, - { - schema: GraphQLSchema - document: string | DocumentNode - variables: StandardScalarVariables - operationName?: string - } - > +export type HookDefPack = { + input: + & InterfaceInput + & TransportInput< + { url: string | URL; headers?: HeadersInit; body: BodyInit }, + { + schema: GraphQLSchema + query: string + variables?: StandardScalarVariables + operationName?: string + } + > +} -export type HookInputUnpack = - & InterfaceInput - & TransportInput< - { response: Response }, - { - result: ExecutionResult - } - > +type RequestInput = { + url: string | URL + method: + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'head' + | 'options' + | 'trace' + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH' + | 'HEAD' + | 'OPTIONS' + | 'TRACE' + headers?: HeadersInit + body: BodyInit +} -export type HookInputDecode = - & { result: ExecutionResult } - & InterfaceInput +export type HookDefExchange = { + input: + & InterfaceInput + & TransportInput< + { + request: RequestInput + }, + { + schema: GraphQLSchema + query: string | DocumentNode + variables?: StandardScalarVariables + operationName?: string + } + > +} -export type Hooks = { - encode: HookInputEncode - pack: HookInputPack - exchange: ExchangeInputHook - unpack: HookInputUnpack - decode: HookInputDecode +export type HookDefUnpack = { + input: + & InterfaceInput + & TransportInput< + { response: Response }, + { + result: ExecutionResult + } + > +} + +export type HookDefDecode = { + input: + & { result: ExecutionResult } + & InterfaceInput } -export const anyware = Anyware.create({ +export type HookMap = { + encode: HookDefEncode + pack: HookDefPack + exchange: HookDefExchange + unpack: HookDefUnpack + decode: HookDefDecode +} + +export const anyware = Anyware.create({ hookNamesOrderedBySequence, hooks: { - encode: ( - input, - ) => { - // console.log(`encode:1`) - let document: string | DocumentNode - switch (input.interface) { - case `raw`: { - document = input.document - break - } - case `typed`: { - // todo turn inputs into variables - document = SelectionSet.Print.rootTypeSelectionSet( - input.context, - getRootIndexOrThrow(input.context, input.rootTypeName), - input.selection, - ) - break - } - default: - throw casesExhausted(input) - } + encode: { + slots: { + body: (input) => { + return JSON.stringify({ + query: input.query, + variables: input.variables, + operationName: input.operationName, + }) + }, + }, + run: ({ input, slots }) => { + let document: string + let variables: StandardScalarVariables | undefined = undefined - // console.log(`encode:2`) - switch (input.transport) { - case `http`: { - return { - ...input, - transport: input.transport, - url: input.schema, - document, - variables: {}, - // operationName: '', + switch (input.interface) { + case `raw`: { + const documentPrinted = typeof input.document === `string` + ? input.document + : print(input.document) + document = documentPrinted + variables = input.variables + break } - } - case `memory`: { - return { - ...input, - transport: input.transport, - schema: input.schema, - document, - variables: {}, - // operationName: '', + case `typed`: { + // todo turn inputs into variables + variables = undefined + document = SelectionSet.Print.rootTypeSelectionSet( + input.context, + getRootIndexOrThrow(input.context, input.rootTypeName), + input.selection, + ) + break } + default: + throw casesExhausted(input) } - } - }, - pack: (input) => { - // console.log(`pack:1`) - const documentPrinted = typeof input.document === `string` - ? input.document - : print(input.document) - switch (input.transport) { - case `http`: { - const body = { - query: documentPrinted, - variables: input.variables, - operationName: input.operationName, + switch (input.transport) { + case `http`: { + return { + ...input, + url: input.schema, + body: slots.body({ + query: document, + variables, + operationName: `todo`, + }), + } } - - const bodyEncoded = JSON.stringify(body) - - const requestConfig = new Request(input.url, { - method: `POST`, - headers: new Headers({ - 'accept': CONTENT_TYPE_GQL, - ...Object.fromEntries(new Headers(input.headers).entries()), - }), - body: bodyEncoded, - }) - - return { - ...input, - request: requestConfig, + case `memory`: { + return { + ...input, + schema: input.schema, + query: document, + variables, + // operationName: '', + } } } + }, + }, + pack: ({ input }) => { + switch (input.transport) { case `memory`: { + return input + } + case `http`: { + const headers = new Headers(input.headers) + headers.append(`accept`, CONTENT_TYPE_GQL) return { ...input, + request: { + url: input.url, + body: input.body, // JSON.stringify({ query, variables, operationName }), + method: `POST`, + headers, + }, } } default: throw casesExhausted(input) } }, - exchange: async (input) => { + exchange: async ({ input }) => { switch (input.transport) { case `http`: { - const response = await fetch(input.request) + const response = await fetch( + new Request(input.request.url, { + method: input.request.method, + headers: input.request.headers, + body: input.request.body, + }), + ) return { ...input, response, @@ -199,7 +248,7 @@ export const anyware = Anyware.create({ case `memory`: { const result = await execute({ schema: input.schema, - document: input.document, + document: input.query, variables: input.variables, operationName: input.operationName, }) @@ -212,9 +261,12 @@ export const anyware = Anyware.create({ throw casesExhausted(input) } }, - unpack: async (input) => { + unpack: async ({ input }) => { switch (input.transport) { case `http`: { + // todo 1 if response is missing header of content length then .json() hangs forever. + // todo 1 firstly consider a timeout, secondly, if response is malformed, then don't even run .json() + // todo 2 if response is e.g. 404 with no json body, then an error is thrown because json parse cannot work, not gracefully handled here const json = await input.response.json() as object const result = parseExecutionResult(json) return { @@ -232,7 +284,7 @@ export const anyware = Anyware.create({ throw casesExhausted(input) } }, - decode: (input) => { + decode: ({ input }) => { switch (input.interface) { // todo this depends on the return mode case `raw`: { diff --git a/src/layers/5_createExtension/createExtension.ts b/src/layers/5_createExtension/createExtension.ts new file mode 100644 index 000000000..0c742381b --- /dev/null +++ b/src/layers/5_createExtension/createExtension.ts @@ -0,0 +1,3 @@ +import type { Extension } from '../5_client/client.js' + +export const createExtension = (input: Extension) => input diff --git a/src/layers/6_extensions/Upload/Upload.spec.ts b/src/layers/6_extensions/Upload/Upload.spec.ts new file mode 100644 index 000000000..2c4ee4fa5 --- /dev/null +++ b/src/layers/6_extensions/Upload/Upload.spec.ts @@ -0,0 +1,71 @@ +// todo in order to test jsdom, we need to boot the server in a separate process +// @vitest-environment node + +import getPort from 'get-port' +import type { Server } from 'node:http' +import { createServer } from 'node:http' +import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest' +import { schema } from '../../../../tests/_/schemaUpload/schema.js' +import { Graffle } from '../../../entrypoints/alpha/main.js' +import { Upload } from './Upload.js' + +import { createYoga } from 'graphql-yoga' +import type { Client } from '../../5_client/client.js' + +let server: Server +let port: number +let graffle: Client + +beforeAll(async () => { + const yoga = createYoga({ schema }) + server = createServer(yoga) // eslint-disable-line + port = await getPort({ port: [3000, 3001, 3002, 3003, 3004] }) + server.listen(port) + await new Promise((resolve) => + server.once(`listening`, () => { + resolve(undefined) + }) + ) +}) + +beforeEach(() => { + graffle = Graffle.create({ + schema: new URL(`http://localhost:${String(port)}/graphql`), + }).use(Upload) +}) + +afterAll(async () => { + await new Promise((resolve) => { + server.close(resolve) + setImmediate(() => { + server.emit(`close`) + }) + }) +}) + +test(`upload`, async () => { + const result = await graffle.raw({ + document: ` + mutation ($blob: Upload!) { + readTextFile(blob: $blob) + } + `, + variables: { + blob: new Blob([`Hello World`], { type: `text/plain` }) as any, // eslint-disable-line + }, + }) + expect(result).toMatchInlineSnapshot(` + { + "data": { + "readTextFile": "Hello World", + }, + "errors": undefined, + "extensions": undefined, + } + `) +}) + +// todo test that non-upload requests work + +// todo test with non-raw +// ^ for this to work we need to generate documents that use variables diff --git a/src/layers/6_extensions/Upload/Upload.ts b/src/layers/6_extensions/Upload/Upload.ts new file mode 100644 index 000000000..39d5d6004 --- /dev/null +++ b/src/layers/6_extensions/Upload/Upload.ts @@ -0,0 +1,61 @@ +import type { StandardScalarVariables } from '../../../lib/graphql.js' +import type { ExecutionInput } from '../../../lib/graphqlHTTP.js' +import { createExtension } from '../../5_createExtension/createExtension.js' +import extractFiles from './extractFiles.js' + +/** + * @see https://github.com/jaydenseric/graphql-multipart-request-spec + */ +export const Upload = createExtension({ + name: `Upload`, + anyware: async ({ encode }) => { + return await encode({ + using: { + body: (input) => { + if (!(input.variables && isUsingUploadScalar(input.variables))) return + + // TODO we can probably get file upload working for in-memory schemas too :) + if (encode.input.transport !== `http`) throw new Error(`Must use http transport for uploads.`) + + return createUploadBody({ + query: input.query, + variables: input.variables, + }) + }, + }, + }) + }, +}) + +const createUploadBody = (input: ExecutionInput): FormData => { + const { clone, files } = extractFiles( + { query: input.query, variables: input.variables }, + (value: unknown) => value instanceof Blob, + ``, + ) + const operationJSON = JSON.stringify(clone) + + if (files.size === 0) throw new Error(`Not an upload request.`) + + const form = new FormData() + + form.append(`operations`, operationJSON) + + const map: Record = {} + let i = 0 + for (const paths of files.values()) { + map[++i] = paths + } + form.append(`map`, JSON.stringify(map)) + + i = 0 + for (const file of files.keys()) { + form.append(String(++i), file) + } + + return form +} + +const isUsingUploadScalar = (_variables: StandardScalarVariables) => { + return Object.values(_variables).some(_ => _ instanceof Blob) +} diff --git a/src/layers/6_extensions/Upload/extractFiles.ts b/src/layers/6_extensions/Upload/extractFiles.ts new file mode 100644 index 000000000..9b3e0856f --- /dev/null +++ b/src/layers/6_extensions/Upload/extractFiles.ts @@ -0,0 +1,201 @@ +/* eslint-disable */ + +import isPlainObject from 'is-plain-obj' + +/** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */ + +/** + * Recursively extracts files and their {@link ObjectPath object paths} within a + * value, replacing them with `null` in a deep clone without mutating the + * original value. + * [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/Filelist) + * instances are treated as + * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) instance + * arrays. + * @template Extractable Extractable file type. + * @param {unknown} value Value to extract files from. Typically an object tree. + * @param {(value: unknown) => value is Extractable} isExtractable Matches + * extractable files. Typically {@linkcode isExtractableFile}. + * @param {ObjectPath} [path] Prefix for object paths for extracted files. + * Defaults to `""`. + * @returns {Extraction} Extraction result. + * @example + * Extracting files from an object. + * + * For the following: + * + * ```js + * import extractFiles from "extract-files/extractFiles.mjs"; + * import isExtractableFile from "extract-files/isExtractableFile.mjs"; + * + * const file1 = new File(["1"], "1.txt", { type: "text/plain" }); + * const file2 = new File(["2"], "2.txt", { type: "text/plain" }); + * const value = { + * a: file1, + * b: [file1, file2], + * }; + * + * const { clone, files } = extractFiles(value, isExtractableFile, "prefix"); + * ``` + * + * `value` remains the same. + * + * `clone` is: + * + * ```json + * { + * "a": null, + * "b": [null, null] + * } + * ``` + * + * `files` is a + * [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + * instance containing: + * + * | Key | Value | + * | :------ | :--------------------------- | + * | `file1` | `["prefix.a", "prefix.b.0"]` | + * | `file2` | `["prefix.b.1"]` | + */ +export default function extractFiles(value: any, isExtractable: any, path = ``): { + clone: object + files: Map +} { + if (!arguments.length) throw new TypeError(`Argument 1 \`value\` is required.`) + + if (typeof isExtractable !== `function`) { + throw new TypeError(`Argument 2 \`isExtractable\` must be a function.`) + } + + if (typeof path !== `string`) { + throw new TypeError(`Argument 3 \`path\` must be a string.`) + } + + /** + * Deeply clonable value. + * @typedef {Array | FileList | { + * [key: PropertyKey]: unknown + * }} Cloneable + */ + + /** + * Clone of a {@link Cloneable deeply cloneable value}. + * @typedef {Exclude} Clone + */ + + /** + * Map of values recursed within the input value and their clones, for reusing + * clones of values that are referenced multiple times within the input value. + * @type {Map} + */ + const clones = new Map() + + /** + * Extracted files and their object paths within the input value. + * @type {Extraction["files"]} + */ + const files = new Map() + + /** + * Recursively clones the value, extracting files. + * @param {unknown} value Value to extract files from. + * @param {ObjectPath} path Prefix for object paths for extracted files. + * @param {Set} recursed Recursed values for avoiding infinite + * recursion of circular references within the input value. + * @returns {unknown} Clone of the value with files replaced with `null`. + */ + function recurse(value: any, path: any, recursed: any) { + if (isExtractable(value)) { + const filePaths = files.get(value) + + filePaths ? filePaths.push(path) : files.set(value, [path]) + + return null + } + + const valueIsList = Array.isArray(value) + || (typeof FileList !== `undefined` && value instanceof FileList) + const valueIsPlainObject = isPlainObject(value) + + if (valueIsList || valueIsPlainObject) { + let clone = clones.get(value) + + const uncloned = !clone + + if (uncloned) { + clone = valueIsList + ? [] + // Replicate if the plain object is an `Object` instance. + : value instanceof /** @type {any} */ (Object) + ? {} + : Object.create(null) + + clones.set(value, /** @type {Clone} */ (clone)) + } + + if (!recursed.has(value)) { + const pathPrefix = path ? `${path}.` : `` + const recursedDeeper = new Set(recursed).add(value) + + if (valueIsList) { + let index = 0 + + for (const item of value) { + const itemClone = recurse( + item, + pathPrefix + index++, + recursedDeeper, + ) + + if (uncloned) /** @type {Array} */ (clone).push(itemClone) + } + } else { + for (const key in value) { + const propertyClone = recurse( + value[key], + pathPrefix + key, + recursedDeeper, + ) + + if (uncloned) { + /** @type {{ [key: PropertyKey]: unknown }} */ + clone[key] = propertyClone + } + } + } + } + + return clone + } + + return value + } + + return { + clone: recurse(value, path, new Set()), + files, + } +} + +/** + * An extraction result. + * @template [Extractable=unknown] Extractable file type. + * @typedef {object} Extraction + * @prop {unknown} clone Clone of the original value with extracted files + * recursively replaced with `null`. + * @prop {Map>} files Extracted files and their + * object paths within the original value. + */ + +/** + * String notation for the path to a node in an object tree. + * @typedef {string} ObjectPath + * @see [`object-path` on npm](https://npm.im/object-path). + * @example + * An object path for object property `a`, array index `0`, object property `b`: + * + * ``` + * a.0.b + * ``` + */ diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts index 6d05d19ac..7c5e936e5 100644 --- a/src/lib/anyware/__.test-d.ts +++ b/src/lib/anyware/__.test-d.ts @@ -1,56 +1,102 @@ /* eslint-disable */ -import { run } from 'node:test' -import { expectTypeOf, test } from 'vitest' +import { describe, expectTypeOf, test } from 'vitest' import { Result } from '../../../tests/_/schema/generated/SchemaRuntime.js' import { ContextualError } from '../errors/ContextualError.js' import { MaybePromise } from '../prelude.js' import { Anyware } from './__.js' -import { ResultEnvelop, SomeHook } from './main.js' +import { SomeHook } from './main.js' type InputA = { valueA: string } type InputB = { valueB: string } type Result = { return: string } -const create = Anyware.create<['a', 'b'], { a: InputA; b: InputB }, Result> - -test('create', () => { - expectTypeOf(create).toMatchTypeOf< - (input: { - hookNamesOrderedBySequence: ['a', 'b'] - hooks: { - a: (input: InputA) => InputB - b: (input: InputB) => Result - } - }) => any - >() +const create = Anyware.create<['a', 'b'], { a: { input: InputA }; b: { input: InputB } }, Result> + +describe('without slots', () => { + test('create', () => { + expectTypeOf(create).toMatchTypeOf< + (input: { + hookNamesOrderedBySequence: ['a', 'b'] + hooks: { + a: (input: { input: InputA }) => InputB + b: (input: { input: InputB }) => Result + } + }) => any + >() + }) + + test('run', () => { + type run = ReturnType['run'] + + expectTypeOf().toMatchTypeOf< + (input: { + initialInput: InputA + options?: Anyware.Options + retryingExtension?: (input: { + a: SomeHook< + (input?: { input?: InputA }) => MaybePromise< + Error | { + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + } + > + > + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + }) => Promise + extensions: ((input: { + a: SomeHook< + (input?: { input?: InputA }) => MaybePromise<{ + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + }> + > + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + }) => Promise)[] + }) => Promise + >() + }) }) -test('run', () => { - type run = ReturnType['run'] - - expectTypeOf().toMatchTypeOf< - (input: { - initialInput: InputA - options?: Anyware.Options - retryingExtension?: (input: { - a: SomeHook< - (input?: InputA) => MaybePromise< - Error | { - b: SomeHook<(input?: InputB) => MaybePromise> +describe('withSlots', () => { + const create = Anyware.create<['a'], { a: { input: InputA; slots: { x: (x: boolean) => number } } }, Result> + + test('create', () => { + expectTypeOf(create).toMatchTypeOf< + (input: { + hookNamesOrderedBySequence: ['a'] + hooks: { + a: { + run: (input: { input: InputA }) => Result + slots: { + x: (x: boolean) => number } + } + } + }) => any + >() + }) + + test('run', () => { + type run = ReturnType['run'] + + expectTypeOf().toMatchTypeOf< + (input: { + initialInput: InputA + options?: Anyware.Options + extensions: ((input: { + a: SomeHook< + ( + input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }, + ) => MaybePromise + > + }) => Promise)[] + retryingExtension?: (input: { + a: SomeHook< + (input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }) => MaybePromise< + Error | Result + > > - > - b: SomeHook<(input?: InputB) => MaybePromise> - }) => Promise - extensions: ((input: { - a: SomeHook< - (input?: InputA) => MaybePromise<{ - b: SomeHook<(input?: InputB) => MaybePromise> - }> - > - b: SomeHook<(input?: InputB) => MaybePromise> - }) => Promise)[] - }) => Promise - >() + }) => Promise + }) => Promise + >() + }) }) diff --git a/src/lib/anyware/main.test.ts b/src/lib/anyware/main.test.ts index 8be1e4642..bbb040a4e 100644 --- a/src/lib/anyware/main.test.ts +++ b/src/lib/anyware/main.test.ts @@ -18,12 +18,13 @@ describe(`one extension`, () => { expect( await run(async ({ a }) => { const { b } = await a(a.input) - await b(b.input) + await b({ input: b.input }) return 0 }), ).toEqual(0) - expect(core.hooks.a).toHaveBeenCalled() - expect(core.hooks.b).toHaveBeenCalled() + expect(core.hooks.a.run.mock.calls[0]).toMatchObject([{ input: { value: `initial` } }]) + expect(core.hooks.a.run).toHaveBeenCalled() + expect(core.hooks.b.run).toHaveBeenCalled() }) test('can call hook with no input, making the original input be used', () => { expect( @@ -43,8 +44,8 @@ describe(`one extension`, () => { return a.input }), ).toEqual({ value: `initial` }) - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`at start, return own result`, async () => { expect( @@ -53,38 +54,38 @@ describe(`one extension`, () => { return 0 }), ).toEqual(0) - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`after first hook, return own result`, async () => { expect( await run(async ({ a }) => { - const { b } = await a(a.input) + const { b } = await a({ input: a.input }) return b.input.value + `+x` }), ).toEqual(`initial+a+x`) - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) }) describe(`can partially apply`, () => { test(`only first hook`, async () => { expect( await run(async ({ a }) => { - return await a({ value: a.input.value + `+ext` }) + return await a({ input: { value: a.input.value + `+ext` } }) }), ).toEqual({ value: `initial+ext+a+b` }) }) test(`only second hook`, async () => { expect( await run(async ({ b }) => { - return await b({ value: b.input.value + `+ext` }) + return await b({ input: { value: b.input.value + `+ext` } }) }), ).toEqual({ value: `initial+a+ext+b` }) }) test(`only second hook + end`, async () => { expect( await run(async ({ b }) => { - const result = await b({ value: b.input.value + `+ext` }) + const result = await b({ input: { value: b.input.value + `+ext` } }) return result.value + `+end` }), ).toEqual(`initial+a+ext+b+end`) @@ -99,35 +100,35 @@ describe(`two extensions`, () => { const ex2 = vi.fn().mockImplementation(() => 2) expect(await run(ex1, ex2)).toEqual(1) expect(ex2).not.toHaveBeenCalled() - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`each can adjust first hook then passthrough`, async () => { - const ex1 = ({ a }: any) => a({ value: a.input.value + `+ex1` }) - const ex2 = ({ a }: any) => a({ value: a.input.value + `+ex2` }) + const ex1 = ({ a }: any) => a({ input: { value: a.input.value + `+ex1` } }) + const ex2 = ({ a }: any) => a({ input: { value: a.input.value + `+ex2` } }) expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+ex2+a+b` }) }) test(`each can adjust each hook`, async () => { const ex1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) - return await b({ value: b.input.value + `+ex1` }) + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + return await b({ input: { value: b.input.value + `+ex1` } }) } const ex2 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex2` }) - return await b({ value: b.input.value + `+ex2` }) + const { b } = await a({ input: { value: a.input.value + `+ex2` } }) + return await b({ input: { value: b.input.value + `+ex2` } }) } expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+ex2+a+ex1+ex2+b` }) }) test(`second can skip hook a`, async () => { const ex1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) - return await b({ value: b.input.value + `+ex1` }) + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + return await b({ input: { value: b.input.value + `+ex1` } }) } const ex2 = async ({ b }: any) => { - return await b({ value: b.input.value + `+ex2` }) + return await b({ input: { value: b.input.value + `+ex2` } }) } expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+a+ex1+ex2+b` }) }) @@ -142,13 +143,13 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual(2) expect(ex1AfterA).toBe(false) - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`second can short-circuit after hook a`, async () => { let ex1AfterB = false const ex1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) await b({ value: b.input.value + `+ex1` }) ex1AfterB = true } @@ -158,8 +159,8 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual(2) expect(ex1AfterB).toBe(false) - expect(core.hooks.a).toHaveBeenCalledOnce() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).toHaveBeenCalledOnce() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) }) @@ -206,7 +207,7 @@ describe(`errors`, () => { }) test(`if implementation fails, without extensions, result is the error`, async () => { - core.hooks.a.mockReset().mockRejectedValueOnce(oops) + core.hooks.a.run.mockReset().mockRejectedValueOnce(oops) const result = await run() as ContextualError expect({ result, @@ -246,7 +247,7 @@ describe(`errors`, () => { describe('retrying extension', () => { test('if hook fails, extension can retry, then short-circuit', async () => { - core.hooks.a.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) + core.hooks.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) const result = await run(createRetryingExtension(async function foo({ a }) { const result1 = await a() expect(result1).toEqual(oops) @@ -308,3 +309,35 @@ describe('retrying extension', () => { }) }) }) + +describe('slots', () => { + test('have defaults that are called by default', async () => { + await run() + expect(core.hooks.a.slots.append.mock.calls[0]).toMatchObject(['a']) + expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) + }) + test('extension can provide own function to slot on just one of a set of hooks', async () => { + const result = await run(async ({ a }) => { + return a({ using: { append: () => 'x' } }) + }) + expect(core.hooks.a.slots.append).not.toBeCalled() + expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) + expect(result).toEqual({ value: 'initial+x+b' }) + }) + test('extension can provide own functions to slots on multiple of a set of hooks', async () => { + const result = await run(async ({ a }) => { + return a({ using: { append: () => 'x', appendExtra: () => '+x2' } }) + }) + expect(result).toEqual({ value: 'initial+x+x2+b' }) + }) + // todo hook with two slots + test('two extensions can each provide own function to same slot on just one of a set of hooks, and the later one wins', async () => { + const result = await run(async ({ a }) => { + const { b } = await a({ using: { append: () => 'x' } }) + return b({ using: { append: () => 'y' } }) + }) + expect(core.hooks.a.slots.append).not.toBeCalled() + expect(core.hooks.b.slots.append).not.toBeCalled() + expect(result).toEqual({ value: 'initial+x+y' }) + }) +}) diff --git a/src/lib/anyware/main.ts b/src/lib/anyware/main.ts index c221d8bc5..e5de1ef0d 100644 --- a/src/lib/anyware/main.ts +++ b/src/lib/anyware/main.ts @@ -29,7 +29,7 @@ export type Extension2< type ExtensionHooks< $HookSequence extends HookSequence, - $HookMap extends Record<$HookSequence[number], object> = Record<$HookSequence[number], object>, + $HookMap extends Record<$HookSequence[number], HookDef> = Record<$HookSequence[number], HookDef>, $Result = unknown, $Options extends ExtensionOptions = ExtensionOptions, > = { @@ -37,7 +37,7 @@ type ExtensionHooks< } type CoreInitialInput<$Core extends Core> = - $Core[PrivateTypesSymbol]['hookMap'][$Core[PrivateTypesSymbol]['hookSequence'][0]] + $Core[PrivateTypesSymbol]['hookMap'][$Core[PrivateTypesSymbol]['hookSequence'][0]]['input'] const PrivateTypesSymbol = Symbol(`private`) @@ -51,21 +51,24 @@ export type SomeHookEnvelope = { [name: string]: SomeHook } -export type SomeHook any = (input: any) => any> = fn & { +export type SomeHook< + fn extends (input?: { input?: any; using?: any }) => any = (input?: { input?: any; using?: any }) => any, +> = fn & { [hookSymbol]: HookSymbol // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. // E.g. adding `| unknown` would destroy the knowledge of hook envelope case // todo this is not strictly true, it could also be the final result - // TODO how do I make this input type object without breaking the final types in e.g. client.extend.test - // Ask Pierre - // (input: object): SomeHookEnvelope - input: Parameters[0] + input: Exclude[0], undefined>['input'] } export type HookMap<$HookSequence extends HookSequence> = Record< $HookSequence[number], - any /* object <- type error but more accurate */ + HookDef > +export type HookDef = { + input: any /* object <- type error but more accurate */ + slots?: any /* object <- type error but more accurate */ +} type Hook< $HookSequence extends HookSequence, @@ -74,21 +77,29 @@ type Hook< $Name extends $HookSequence[number] = $HookSequence[number], $Options extends ExtensionOptions = ExtensionOptions, > = - & (<$$Input extends $HookMap[$Name]>( - input?: $$Input, + & (<$$Input extends $HookMap[$Name]['input']>( + input?: { + input?: $$Input + } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), // eslint-disable-line ) => HookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>) & { [hookSymbol]: HookSymbol - input: $HookMap[$Name] + input: $HookMap[$Name]['input'] } +type SlotInputify<$Slots extends Record any>> = { + [K in keyof $Slots]?: SlotInput<$Slots[K]> +} + +type SlotInput any> = (...args: Parameters) => ReturnType | undefined + type HookReturn< $HookSequence extends HookSequence, $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, $Result = unknown, $Name extends $HookSequence[number] = $HookSequence[number], $Options extends ExtensionOptions = ExtensionOptions, -> = +> = Promise< | ($Options['retrying'] extends true ? Error : never) | (IsLastValue<$Name, $HookSequence> extends true ? $Result : { [$NameNext in FindValueAfter<$Name, $HookSequence>]: Hook< @@ -98,6 +109,7 @@ type HookReturn< $NameNext > }) +> export type Core< $HookSequence extends HookSequence = HookSequence, @@ -111,11 +123,44 @@ export type Core< } hookNamesOrderedBySequence: $HookSequence hooks: { - [$HookName in $HookSequence[number]]: ( - input: $HookMap[$HookName], - ) => MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true ? $Result : $HookMap[FindValueAfter<$HookName, $HookSequence>] - > + [$HookName in $HookSequence[number]]: { + slots: $HookMap[$HookName]['slots'] + run: (input: { + input: $HookMap[$HookName]['input'] + slots: $HookMap[$HookName]['slots'] + }) => MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>] + > + } + } +} + +export type CoreInput< + $HookSequence extends HookSequence = HookSequence, + $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, + $Result = unknown, +> = { + hookNamesOrderedBySequence: $HookSequence + hooks: { + [$HookName in $HookSequence[number]]: keyof $HookMap[$HookName]['slots'] extends never ? (input: { + input: $HookMap[$HookName]['input'] + slots: $HookMap[$HookName]['slots'] + } + ) => MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] + > + : { + slots: $HookMap[$HookName]['slots'] + run: (input: { + input: $HookMap[$HookName]['input'] + slots: $HookMap[$HookName]['slots'] + }) => MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] + > + } } } @@ -179,7 +224,7 @@ const createPassthrough = (hookName: string) => async (hookEnvelope: SomeHookEnv if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) } - return await hook(hook.input) + return await hook({ input: hook.input }) // eslint-disable-line } type Config = Required @@ -214,16 +259,22 @@ export const create = < $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, $Result = unknown, >( - coreInput: Omit, PrivateTypesSymbol>, + coreInput: CoreInput<$HookSequence, $HookMap, $Result>, ): Builder> => { type $Core = Core<$HookSequence, $HookMap, $Result> - const core = coreInput as any as $Core + const core = { + ...coreInput, + hooks: Object.fromEntries( + Object.entries(coreInput.hooks).map(([k, v]) => { + return [k, typeof v === `function` ? { slots: {}, run: v } : v] + }), + ), + } as any as $Core const builder: Builder<$Core> = { core, - run: async (input) => { - const { initialInput, extensions, options, retryingExtension } = input + run: async ({ initialInput, extensions, options, retryingExtension }) => { const extensions_ = retryingExtension ? [...extensions, createRetryingExtension(retryingExtension)] : extensions const initialHookStackAndErrors = extensions_.map(extension => toInternalExtension(core, resolveOptions(options), extension) diff --git a/src/lib/anyware/runHook.ts b/src/lib/anyware/runHook.ts index a28048f4b..ad98d7841 100644 --- a/src/lib/anyware/runHook.ts +++ b/src/lib/anyware/runHook.ts @@ -1,6 +1,6 @@ import { Errors } from '../errors/__.js' -import type { Deferred } from '../prelude.js' -import { casesExhausted, createDeferred, debug, debugSub, errorFromMaybeError } from '../prelude.js' +import type { Deferred, SomeFunction } from '../prelude.js' +import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../prelude.js' import type { Core, Extension, ResultEnvelop, SomeHookEnvelope } from './main.js' type HookDoneResolver = (input: HookResult) => void @@ -29,11 +29,14 @@ export type HookResultErrorImplementation = { error: Error } +type Slots = Record + type Input = { core: Core name: string done: HookDoneResolver originalInput: unknown + customSlots: Slots /** * The extensions that are at this hook awaiting. */ @@ -55,7 +58,7 @@ const createExecutableChunk = <$Extension extends Extension>(extension: $Extensi }) export const runHook = async ( - { core, name, done, originalInput, extensionsStack, nextExtensionsStack, asyncErrorDeferred }: Input, + { core, name, done, originalInput, extensionsStack, nextExtensionsStack, asyncErrorDeferred, customSlots }: Input, ) => { const debugHook = debugSub(`hook ${name}:`) @@ -88,9 +91,13 @@ export const runHook = async ( debugExtension(`start`) let hookFailed = false const hook = createHook(originalInput, (extensionInput) => { - debugExtension(`extension calls this hook`) + debugExtension(`extension calls this hook`, extensionInput) - const inputResolved = extensionInput ?? originalInput + const inputResolved = extensionInput?.input ?? originalInput + const customSlotsResolved = { + ...customSlots, + ...extensionInput?.using, + } // [1] // Never resolve this hook call, the extension is in an invalid state and should not continue executing. @@ -135,12 +142,14 @@ export const runHook = async ( asyncErrorDeferred, extensionsStack: [extensionRetry], nextExtensionsStack, + customSlots: customSlotsResolved, }) return extensionRetry.currentChunk.promise.then(async (envelope) => { const envelop_ = envelope as SomeHookEnvelope // todo ... better way? const hook = envelop_[name] if (!hook) throw new Error(`Hook not found in envelope: ${name}`) - const result = await hook(extensionInput ?? originalInput) as Promise< + // todo use inputResolved ? + const result = await hook({ ...extensionInput, input: extensionInput?.input ?? originalInput }) as Promise< SomeHookEnvelope | Error | ResultEnvelop > return result @@ -158,6 +167,7 @@ export const runHook = async ( originalInput: inputResolved, extensionsStack: extensionsStackRest, nextExtensionsStack: nextNextHookStack, + customSlots: customSlotsResolved, }) return extensionWithNextChunk.currentChunk.promise.then(_ => { @@ -201,7 +211,7 @@ export const runHook = async ( return } case `extensionReturned`: { - debug(`${name}: ${extension.name}: extension returned`) + debugExtension(`extension returned`) if (result === envelope) { void runHook({ core, @@ -211,6 +221,7 @@ export const runHook = async ( asyncErrorDeferred, extensionsStack: extensionsStackRest, nextExtensionsStack, + customSlots, }) } else { done({ type: `shortCircuited`, result }) @@ -218,7 +229,7 @@ export const runHook = async ( return } case `extensionThrew`: { - debug(`${name}: ${extension.name}: extension threw`) + debugExtension(`extension threw`) done({ type: `error`, hookName: name, @@ -229,7 +240,7 @@ export const runHook = async ( return } case `hookInvokedButThrew`: - debug(`${name}: ${extension.name}: hook error`) + debugExtension(`hook error`) // todo rename source to "hook" done({ type: `error`, hookName: name, source: `implementation`, error: errorFromMaybeError(result) }) return @@ -246,7 +257,11 @@ export const runHook = async ( let result try { - result = await implementation(originalInput as any) + const slotsResolved = { + ...implementation.slots as Slots, // todo is this cast needed, can we Slots type the property? + ...customSlots, + } + result = await implementation.run({ input: originalInput, slots: slotsResolved }) } catch (error) { debugHook(`implementation error`) const lastExtension = nextExtensionsStack[nextExtensionsStack.length - 1] @@ -266,7 +281,7 @@ export const runHook = async ( } } -const createHook = <$X, $F extends (input?: object) => any>( +const createHook = <$X, $F extends (input?: HookInput) => any>( originalInput: $X, fn: $F, ): $F & { input: $X } => { @@ -275,3 +290,8 @@ const createHook = <$X, $F extends (input?: object) => any>( // @ts-expect-error return fn } + +type HookInput = { + input?: object + using?: Slots +} diff --git a/src/lib/anyware/runPipeline.ts b/src/lib/anyware/runPipeline.ts index 6118de2a6..c8687d866 100644 --- a/src/lib/anyware/runPipeline.ts +++ b/src/lib/anyware/runPipeline.ts @@ -36,6 +36,7 @@ export const runPipeline = async ( originalInput, extensionsStack, asyncErrorDeferred, + customSlots: {}, nextExtensionsStack: [], }) diff --git a/src/lib/anyware/specHelpers.ts b/src/lib/anyware/specHelpers.ts index 5568f30d9..8768710f8 100644 --- a/src/lib/anyware/specHelpers.ts +++ b/src/lib/anyware/specHelpers.ts @@ -3,25 +3,63 @@ import { beforeEach, vi } from 'vitest' import { Anyware } from './__.js' import { type ExtensionInput, type Options } from './main.js' -export type Input = { value: string } -export const initialInput: Input = { value: `initial` } +export type Input = { + input: { value: string } + slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } +} + +export const initialInput: Input['input'] = { value: `initial` } // export type $Core = Core<['a', 'b'],Anyware.HookMap<['a','b']>,Input> type $Core = ReturnType & { hooks: { - a: Mock - b: Mock + a: { + run: Mock + slots: { + append: Mock<[hookName: string], string> + appendExtra: Mock<[hookName: string], string> + } + } + b: { + run: Mock + slots: { + append: Mock<[hookName: string], string> + appendExtra: Mock<[hookName: string], string> + } + } } } export const createAnyware = () => { - const a = vi.fn().mockImplementation((input: Input) => { - return { value: input.value + `+a` } - }) - const b = vi.fn().mockImplementation((input: Input) => { - return { value: input.value + `+b` } - }) + const a = { + slots: { + append: vi.fn().mockImplementation((hookName: string) => { + return hookName + }), + appendExtra: vi.fn().mockImplementation(() => { + return `` + }), + }, + run: vi.fn().mockImplementation(({ input, slots }: Input) => { + const extra = slots.appendExtra(`a`) + return { value: input.value + `+` + slots.append(`a`) + extra } + }), + } + const b = { + slots: { + append: vi.fn().mockImplementation((hookName: string) => { + return hookName + }), + appendExtra: vi.fn().mockImplementation(() => { + return `` + }), + }, + run: vi.fn().mockImplementation(({ input, slots }: Input) => { + const extra = slots.appendExtra(`b`) + return { value: input.value + `+` + slots.append(`b`) + extra } + }), + } return Anyware.create<['a', 'b'], Anyware.HookMap<['a', 'b']>, Input>({ hookNamesOrderedBySequence: [`a`, `b`], diff --git a/src/lib/graphqlHTTP.ts b/src/lib/graphqlHTTP.ts index e301319dd..b10661962 100644 --- a/src/lib/graphqlHTTP.ts +++ b/src/lib/graphqlHTTP.ts @@ -1,7 +1,14 @@ import type { GraphQLFormattedError } from 'graphql' import { type ExecutionResult, GraphQLError } from 'graphql' +import type { StandardScalarVariables } from './graphql.js' import { isPlainObject } from './prelude.js' +export type ExecutionInput = { + query: string + variables: StandardScalarVariables + operationName?: string +} + export const parseExecutionResult = (result: unknown): ExecutionResult => { if (typeof result !== `object` || result === null) { throw new Error(`Invalid execution result: result is not object`) diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 109a9784e..8bb0dbbbe 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -241,7 +241,7 @@ export const capitalizeFirstLetter = (string: string) => string.charAt(0).toUppe export type SomeAsyncFunction = (...args: unknown[]) => Promise -export type SomeMaybeAsyncFunction = (...args: unknown[]) => MaybePromise +export type SomeFunction = (...args: unknown[]) => MaybePromise export type Deferred = { promise: Promise diff --git a/tests/_/schemaUpload/schema.ts b/tests/_/schemaUpload/schema.ts new file mode 100644 index 000000000..a764932c9 --- /dev/null +++ b/tests/_/schemaUpload/schema.ts @@ -0,0 +1,36 @@ +import SchemaBuilder from '@pothos/core' + +const builder = new SchemaBuilder<{ + Scalars: { Upload: { Input: Blob; Output: never } } +}>({}) + +builder.scalarType(`Upload`, { + serialize: () => { + throw new Error(`Uploads can only be used as input types`) + }, +}) + +builder.queryType({ + fields: t => ({ + greetings: t.string({ resolve: () => `Hello World` }), + }), +}) + +builder.mutationType({ + fields: t => ({ + readTextFile: t.string({ + args: { + blob: t.arg({ + type: `Upload`, + required: true, + }), + }, + resolve: async (_, { blob }) => { + const textContent = await blob.text() + return textContent + }, + }), + }), +}) + +export const schema = builder.toSchema({}) diff --git a/tests/_/text.txt b/tests/_/text.txt new file mode 100644 index 000000000..5adcd3d86 --- /dev/null +++ b/tests/_/text.txt @@ -0,0 +1 @@ +This is a text file.