diff --git a/DOCUMENTATION_NEXT.md b/DOCUMENTATION_NEXT.md index ee9c2e7d8..1e2ed6f0b 100644 --- a/DOCUMENTATION_NEXT.md +++ b/DOCUMENTATION_NEXT.md @@ -1,56 +1,115 @@ # GraphQL Request Documentation -# Return Mode +# Output -GraphQL execution has this general pattern: +The standard GraphQL execution result type in the JavaScript ecosystem (from the `graphql` package) has roughly this type: ```ts interface GraphQLExecutionResult { data?: object errors?: GraphQLError[] - extensions?: [] + extensions?: unknown[] } ``` -You can change the output of client methods by configuring its return mode. This allows you to tailor the client better to your specific use-case. +Graffle can return this type but also many other types depending on your configuration. For example: -The only client method that is not affected by return mode is `raw` which will _always_ return a standard GraphQL result type. +1. Return the data directly without an envelope. +1. Return all or some categories of errors (return type becomes a union). +1. Return an envelope and place all or some categories of errors into the `errors` field. +1. Throw all or some categories of errors. -Here is a summary table of the modes: +Configuration can be done at the constructor level. Method level will also be supported in the future. -| Mode | Throw Sources (no type safety) | Returns (type safe) | -| ---------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -| `graphql` | Extensions, Fetch | `GraphQLExecutionResult` | -| `graphqlSuccess` | Extensions, Fetch, GraphQLExecutionResult.errors | `GraphQLExecutionResult` with `.errors` always missing. | -| `data` (default) | Extensions, Fetch, GraphQLExecutionResult.errors | `GraphQLExecutionResult.data` | -| `dataSuccess` | Extensions, Fetch, GraphQLExecutionResult.errors, GraphQLExecutionResult.data Schema Errors | `GraphQLExecutionResult.data` without any schema errors | -| `dataAndErrors` | | `GraphQLExecutionResult.data`, errors from: Extensions, Fetch, GraphQLExecutionResult.errors | +```ts +// Constructor Level + +const graffle = Graffle.create({ + output: { + errors: { + execution: 'throw', + other: 'return', + }, + }, +}) + +// Method Level (planned, not implemented yet) + +await graffle.query.foo({}, { + output: { + envelope: true, + }, +}) +``` -## `graphql` +## Errors -Return the standard graphql execution output. +There are three categories of errors: -## `graphqlSuccess` +1. `execution` – Anything that went wrong during execution. Examples: invalid input given, resolver threw an error. +2. `schema` – Only present if the [schema errors](#schema-errors) are being used. Any time a result field returns an error type. +3. `other` – Anything else. Examples: network error during request, extension threw error, your anyware threw an error. -Return the standard graphql execution output. However, if there would be any errors then they're thrown as an `AggregateError`. -This mode acts like you were using [`OrThrow`](#orthrow) method variants all the time. +You can choose to output error categories in the following ways: -## `dataSuccess` +1. `throw` – Errors from category will be thrown. There is no type safety with this approach. +2. `return` – Errors from category will be returned. The return type will thus become a union type. +3. `default` – Use whatever the default is (you can change the default). -Return just the data excluding [schema errors](#schema-errors). Errors are thrown as an `AggregateError`. -This mode acts like you were using [`OrThrow`](#orthrow) method variants all the time. +## Envelope -This mode is only available when using [schema errors](#schema-errors). +You can choose to use an envelope. When you use an envelope the data will be returned in a `data` property. Additional metadata properties will be exposed: -## `data` +1. `errors` – errors that you have chosen to include in the envelope. +2. `extensions` – GraphQL execution result extensions. +3. `response` – Only present if [transport](#link-todo) is `http`. The HTTP response to the request that was sent for the given GraphQL document. -Return just the data including [schema errors](#schema-errors) (if using). Other errors are thrown as an `AggregateError`. +## Examples -**This mode is the default.** +### Standard GraphQL -## `dataAndErrors` +```ts +const graffle = Graffle.create({ + output: { + envelope: { + errors: { + execution: true, // Bring execution errors into envelope. + }, + }, + errors: { + other: 'throw', + }, + }, +}) + +assertType<{ + data: { + foo: string /* or whatever */ + } + errors: GraphQLError[] + extensions: unknown[] + response: Response // Non-standard. Present when using HTTP transport. +}>(await graffle.query.foo()) +``` -Return a union type of data and errors. This is the most type-safe mode. It never throws. +### Full Type Safety + +```ts +const graffle = Graffle.create({ + output: { + defaults: { + errorChannel: 'return', + }, + envelope: false, + }, +}) + +assertType< + | string /* or whatever */ + | GraphQLError + | Error +>(await graffle.query.foo()) +``` # Schema Errors diff --git a/package.json b/package.json index d9a4a6b4b..1307f1d48 100644 --- a/package.json +++ b/package.json @@ -111,11 +111,12 @@ "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", "@types/json-bigint": "^1.0.4", - "@types/node": "^22.0.0", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@types/node": "^22.0.2", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "doctoc": "^2.2.1", "dripip": "^0.10.0", + "es-toolkit": "^1.13.1", "eslint": "^9.8.0", "eslint-config-prisma": "^0.6.0", "eslint-plugin-deprecation": "^3.0.0", @@ -133,10 +134,10 @@ "jsdom": "^24.1.1", "json-bigint": "^1.0.0", "publint": "^0.2.9", - "tsx": "^4.16.2", + "tsx": "^4.16.5", "type-fest": "^4.23.0", "typescript": "^5.5.4", - "typescript-eslint": "^7.18.0", - "vitest": "^2.0.4" + "typescript-eslint": "^8.0.0", + "vitest": "^2.0.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1ba9c302..8ae4e7e00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,26 +55,29 @@ importers: specifier: ^1.0.4 version: 1.0.4 '@types/node': - specifier: ^22.0.0 - version: 22.0.0 + specifier: ^22.0.2 + version: 22.0.2 '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.0.0 + version: 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.0.0 + version: 8.0.0(eslint@9.8.0)(typescript@5.5.4) doctoc: specifier: ^2.2.1 version: 2.2.1 dripip: specifier: ^0.10.0 version: 0.10.0 + es-toolkit: + specifier: ^1.13.1 + version: 1.13.1 eslint: specifier: ^9.8.0 version: 9.8.0 eslint-config-prisma: specifier: ^0.6.0 - version: 0.6.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-deprecation@3.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3(eslint@9.8.0))(eslint-plugin-tsdoc@0.3.0)(eslint@9.8.0)(typescript@5.5.4) + version: 0.6.0(@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-deprecation@3.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3(eslint@9.8.0))(eslint-plugin-tsdoc@0.3.0)(eslint@9.8.0)(typescript@5.5.4) eslint-plugin-deprecation: specifier: ^3.0.0 version: 3.0.0(eslint@9.8.0)(typescript@5.5.4) @@ -121,8 +124,8 @@ importers: specifier: ^0.2.9 version: 0.2.9 tsx: - specifier: ^4.16.2 - version: 4.16.2 + specifier: ^4.16.5 + version: 4.16.5 type-fest: specifier: ^4.23.0 version: 4.23.0 @@ -130,11 +133,11 @@ importers: specifier: ^5.5.4 version: 5.5.4 typescript-eslint: - specifier: ^7.18.0 - version: 7.18.0(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.0.0 + version: 8.0.0(eslint@9.8.0)(typescript@5.5.4) vitest: - specifier: ^2.0.4 - version: 2.0.4(@types/node@22.0.0)(happy-dom@14.12.3)(jsdom@24.1.1) + specifier: ^2.0.5 + version: 2.0.5(@types/node@22.0.2)(happy-dom@14.12.3)(jsdom@24.1.1) packages: @@ -581,83 +584,83 @@ packages: '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} - '@rollup/rollup-android-arm-eabi@4.19.1': - resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==} + '@rollup/rollup-android-arm-eabi@4.19.2': + resolution: {integrity: sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.19.1': - resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==} + '@rollup/rollup-android-arm64@4.19.2': + resolution: {integrity: sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.19.1': - resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==} + '@rollup/rollup-darwin-arm64@4.19.2': + resolution: {integrity: sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.19.1': - resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==} + '@rollup/rollup-darwin-x64@4.19.2': + resolution: {integrity: sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.19.1': - resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.19.2': + resolution: {integrity: sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.19.1': - resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==} + '@rollup/rollup-linux-arm-musleabihf@4.19.2': + resolution: {integrity: sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.19.1': - resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==} + '@rollup/rollup-linux-arm64-gnu@4.19.2': + resolution: {integrity: sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.19.1': - resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==} + '@rollup/rollup-linux-arm64-musl@4.19.2': + resolution: {integrity: sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': - resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.19.2': + resolution: {integrity: sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.19.1': - resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==} + '@rollup/rollup-linux-riscv64-gnu@4.19.2': + resolution: {integrity: sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.19.1': - resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==} + '@rollup/rollup-linux-s390x-gnu@4.19.2': + resolution: {integrity: sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.19.1': - resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==} + '@rollup/rollup-linux-x64-gnu@4.19.2': + resolution: {integrity: sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.19.1': - resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==} + '@rollup/rollup-linux-x64-musl@4.19.2': + resolution: {integrity: sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.19.1': - resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==} + '@rollup/rollup-win32-arm64-msvc@4.19.2': + resolution: {integrity: sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.19.1': - resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==} + '@rollup/rollup-win32-ia32-msvc@4.19.2': + resolution: {integrity: sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.19.1': - resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==} + '@rollup/rollup-win32-x64-msvc@4.19.2': + resolution: {integrity: sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==} cpu: [x64] os: [win32] @@ -725,8 +728,8 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - '@types/node@22.0.0': - resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + '@types/node@22.0.2': + resolution: {integrity: sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==} '@types/parse-git-config@3.0.4': resolution: {integrity: sha512-jz5eGdk9lBgAd4rMbXTP7MRG7AsGQ8DrXsRumDcXDLClHcpKluislylPVMP/qp90J/LlIrrPZRZIQUflHfrDnQ==} @@ -771,6 +774,17 @@ packages: typescript: optional: true + '@typescript-eslint/eslint-plugin@8.0.0': + resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/experimental-utils@5.0.0': resolution: {integrity: sha512-Dnp4dFIsZcPawD6CT1p5NibNUQyGSEz80sULJZkyhyna8AEqArmfwMwJPbmKzWVo4PabqNVzHYlzmcdLQWk+pg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -797,6 +811,16 @@ packages: typescript: optional: true + '@typescript-eslint/parser@8.0.0': + resolution: {integrity: sha512-pS1hdZ+vnrpDIxuFXYQpLTILglTjSYJ9MbetZctrUawogUsPdz31DIIRZ9+rab0LhYNTsk88w4fIzVheiTbWOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/scope-manager@5.0.0': resolution: {integrity: sha512-5RFjdA/ain/MDUHYXdF173btOKncIrLuBmA9s6FJhzDrRAyVSA+70BHg0/MW6TE+UiKVyRtX91XpVS0gVNwVDQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -805,6 +829,10 @@ packages: resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.0.0': + resolution: {integrity: sha512-V0aa9Csx/ZWWv2IPgTfY7T4agYwJyILESu/PVqFtTFz9RIS823mAze+NbnBI8xiwdX3iqeQbcTYlvB04G9wyQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -815,6 +843,15 @@ packages: typescript: optional: true + '@typescript-eslint/type-utils@8.0.0': + resolution: {integrity: sha512-mJAFP2mZLTBwAn5WI4PMakpywfWFH5nQZezUQdSKV23Pqo6o9iShQg1hP2+0hJJXP2LnZkWPphdIq4juYYwCeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/types@5.0.0': resolution: {integrity: sha512-dU/pKBUpehdEqYuvkojmlv0FtHuZnLXFBn16zsDmlFF3LXkOpkAQ2vrKc3BidIIve9EMH2zfTlxqw9XM0fFN5w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -823,6 +860,10 @@ packages: resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.0.0': + resolution: {integrity: sha512-wgdSGs9BTMWQ7ooeHtu5quddKKs5Z5dS+fHLbrQI+ID0XWJLODGMHRfhwImiHoeO2S5Wir2yXuadJN6/l4JRxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.0.0': resolution: {integrity: sha512-V/6w+PPQMhinWKSn+fCiX5jwvd1vRBm7AX7SJQXEGQtwtBvjMPjaU3YTQ1ik2UF1u96X7tsB96HMnulG3eLi9Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -841,12 +882,27 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.0.0': + resolution: {integrity: sha512-5b97WpKMX+Y43YKi4zVcCVLtK5F98dFls3Oxui8LbnmRsseKenbbDinmvxrWegKDMmlkIq/XHuyy0UGLtpCDKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 + '@typescript-eslint/utils@8.0.0': + resolution: {integrity: sha512-k/oS/A/3QeGLRvOWCg6/9rATJL5rec7/5s1YmdS0ZU6LHveJyGFwBvLhSRBv6i9xaj7etmosp+l+ViN1I9Aj/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/visitor-keys@5.0.0': resolution: {integrity: sha512-yRyd2++o/IrJdyHuYMxyFyBhU762MRHQ/bAGQeTnN3pGikfh+nEmM61XTqaDH1XDp53afZ+waXrk0ZvenoZ6xw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -855,23 +911,27 @@ packages: resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} - '@vitest/expect@2.0.4': - resolution: {integrity: sha512-39jr5EguIoanChvBqe34I8m1hJFI4+jxvdOpD7gslZrVQBKhh8H9eD7J/LJX4zakrw23W+dITQTDqdt43xVcJw==} + '@typescript-eslint/visitor-keys@8.0.0': + resolution: {integrity: sha512-oN0K4nkHuOyF3PVMyETbpP5zp6wfyOvm7tWhTMfoqxSSsPmJIh6JNASuZDlODE8eE+0EB9uar+6+vxr9DBTYOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/pretty-format@2.0.4': - resolution: {integrity: sha512-RYZl31STbNGqf4l2eQM1nvKPXE0NhC6Eq0suTTePc4mtMQ1Fn8qZmjV4emZdEdG2NOWGKSCrHZjmTqDCDoeFBw==} + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} - '@vitest/runner@2.0.4': - resolution: {integrity: sha512-Gk+9Su/2H2zNfNdeJR124gZckd5st4YoSuhF1Rebi37qTXKnqYyFCd9KP4vl2cQHbtuVKjfEKrNJxHHCW8thbQ==} + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} - '@vitest/snapshot@2.0.4': - resolution: {integrity: sha512-or6Mzoz/pD7xTvuJMFYEtso1vJo1S5u6zBTinfl+7smGUhqybn6VjzCDMhmTyVOFWwkCMuNjmNNxnyXPgKDoPw==} + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} - '@vitest/spy@2.0.4': - resolution: {integrity: sha512-uTXU56TNoYrTohb+6CseP8IqNwlNdtPwEO0AWl+5j7NelS6x0xZZtP0bDWaLvOfUbaYwhhWp1guzXUxkC7mW7Q==} + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/utils@2.0.4': - resolution: {integrity: sha512-Zc75QuuoJhOBnlo99ZVUkJIuq4Oj0zAkrQ2VzCqNCx6wAwViHEh5Fnp4fiJTE9rA+sAoXRf00Z9xGgfEzV6fzQ==} + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} '@whatwg-node/events@0.1.2': resolution: {integrity: sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==} @@ -881,13 +941,13 @@ packages: resolution: {integrity: sha512-J+zopRcUVOhkiQYlHpxOEZuOgZtqW9xMaNQFDjESm9vRcyATms+E2/p2mZiVQGllPqWflkA3SzoJC1MxV4Pf9g==} engines: {node: '>=16.0.0'} - '@whatwg-node/node-fetch@0.5.19': - resolution: {integrity: sha512-HSR/d2jGFuI2pcoQr7M92AjB9V7C8k8bFQ4NjJK9fwHyl1DyDnQYe+t1ygD84rCGNV8CIl1+OO5OamnvFzbqDw==} + '@whatwg-node/node-fetch@0.5.20': + resolution: {integrity: sha512-DFLsOG//CrDdIO0x7Q7Ompxj3TZhB4iMDeXpQKY4toSbIbzsKmbwyOkzXMwvV1syxvAtPoHBzyGGtDrPV424FA==} engines: {node: '>=18.0.0'} - '@whatwg-node/server@0.9.45': - resolution: {integrity: sha512-FBDf8aeKZSwFyASKTFIAcNfZPV7jczVItfwimcGFai9pXUMLJC3efjkNMh1HqKzM4sUUkocX8p0kaFzlkqrt1Q==} - engines: {node: '>=16.0.0'} + '@whatwg-node/server@0.9.46': + resolution: {integrity: sha512-vUKCMPP6f2BLtOxnK2c98QmK0rb24RlmXb2enbEg8nXttQLvlKfMOfaY7uNAtaMXejjR2ku/ww9EEeiWXV3Q9A==} + engines: {node: '>=18.0.0'} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1288,6 +1348,9 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + es-toolkit@1.13.1: + resolution: {integrity: sha512-tGsgoI8DfU0yrZI7w97aYVMZJU5sjpXC+HK8aYf3pmLQRNHMleiJN5ud21dA/IHKkTDFY5jcDMQcLs0A21LtAg==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2513,8 +2576,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.19.1: - resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==} + rollup@4.19.2: + resolution: {integrity: sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2769,8 +2832,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.16.2: - resolution: {integrity: sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==} + tsx@4.16.5: + resolution: {integrity: sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==} engines: {node: '>=18.0.0'} hasBin: true @@ -2818,6 +2881,15 @@ packages: typescript: optional: true + typescript-eslint@8.0.0: + resolution: {integrity: sha512-yQWBJutWL1PmpmDddIOl9/Mi6vZjqNCjqSGBMQ4vsc2Aiodk0SnbQQWPXbSy0HNuKCuGkw1+u4aQ2mO40TdhDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + typescript@4.4.3: resolution: {integrity: sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==} engines: {node: '>=4.2.0'} @@ -2910,8 +2982,8 @@ packages: vfile@4.2.1: resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} - vite-node@2.0.4: - resolution: {integrity: sha512-ZpJVkxcakYtig5iakNeL7N3trufe3M6vGuzYAr4GsbCTwobDeyPJpE4cjDhhPluv8OvQCFzu2LWp6GkoKRITXA==} + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -2943,15 +3015,15 @@ packages: terser: optional: true - vitest@2.0.4: - resolution: {integrity: sha512-luNLDpfsnxw5QSW4bISPe6tkxVvv5wn2BBs/PuDRkhXZ319doZyLOBr1sjfB5yCEpTiU7xCAdViM8TNVGPwoog==} + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.0.4 - '@vitest/ui': 2.0.4 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -3560,52 +3632,52 @@ snapshots: '@repeaterjs/repeater@3.0.6': {} - '@rollup/rollup-android-arm-eabi@4.19.1': + '@rollup/rollup-android-arm-eabi@4.19.2': optional: true - '@rollup/rollup-android-arm64@4.19.1': + '@rollup/rollup-android-arm64@4.19.2': optional: true - '@rollup/rollup-darwin-arm64@4.19.1': + '@rollup/rollup-darwin-arm64@4.19.2': optional: true - '@rollup/rollup-darwin-x64@4.19.1': + '@rollup/rollup-darwin-x64@4.19.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.19.1': + '@rollup/rollup-linux-arm-gnueabihf@4.19.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.19.1': + '@rollup/rollup-linux-arm-musleabihf@4.19.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.19.1': + '@rollup/rollup-linux-arm64-gnu@4.19.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.19.1': + '@rollup/rollup-linux-arm64-musl@4.19.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.19.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.19.1': + '@rollup/rollup-linux-riscv64-gnu@4.19.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.19.1': + '@rollup/rollup-linux-s390x-gnu@4.19.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.19.1': + '@rollup/rollup-linux-x64-gnu@4.19.2': optional: true - '@rollup/rollup-linux-x64-musl@4.19.1': + '@rollup/rollup-linux-x64-musl@4.19.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.19.1': + '@rollup/rollup-win32-arm64-msvc@4.19.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.19.1': + '@rollup/rollup-win32-ia32-msvc@4.19.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.19.1': + '@rollup/rollup-win32-x64-msvc@4.19.2': optional: true '@sindresorhus/is@4.6.0': {} @@ -3633,11 +3705,11 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.0.0 + '@types/node': 22.0.2 '@types/connect@3.4.38': dependencies: - '@types/node': 22.0.0 + '@types/node': 22.0.2 '@types/debug@4.1.12': dependencies: @@ -3658,7 +3730,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 22.0.0 + '@types/node': 22.0.2 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -3686,7 +3758,7 @@ snapshots: '@types/ms@0.7.34': {} - '@types/node@22.0.0': + '@types/node@22.0.2': dependencies: undici-types: 6.11.1 @@ -3694,7 +3766,7 @@ snapshots: '@types/parse-github-url@1.0.3': dependencies: - '@types/node': 22.0.0 + '@types/node': 22.0.2 '@types/qs@6.9.15': {} @@ -3703,12 +3775,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.0.0 + '@types/node': 22.0.2 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.0.0 + '@types/node': 22.0.2 '@types/send': 0.17.4 '@types/unist@2.0.10': {} @@ -3748,6 +3820,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/type-utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.0 + eslint: 9.8.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/experimental-utils@5.0.0(eslint@9.8.0)(typescript@4.4.3)': dependencies: '@types/json-schema': 7.0.15 @@ -3786,6 +3876,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.0 + debug: 4.3.6 + eslint: 9.8.0 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@5.0.0': dependencies: '@typescript-eslint/types': 5.0.0 @@ -3796,6 +3899,11 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager@8.0.0': + dependencies: + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/visitor-keys': 8.0.0 + '@typescript-eslint/type-utils@7.18.0(eslint@9.8.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) @@ -3808,10 +3916,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.0.0(eslint@9.8.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + debug: 4.3.6 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + '@typescript-eslint/types@5.0.0': {} '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.0.0': {} + '@typescript-eslint/typescript-estree@5.0.0(typescript@4.4.3)': dependencies: '@typescript-eslint/types': 5.0.0 @@ -3841,6 +3963,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.0.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/visitor-keys': 8.0.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.18.0(eslint@9.8.0)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) @@ -3852,6 +3989,17 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@8.0.0(eslint@9.8.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) + eslint: 9.8.0 + transitivePeerDependencies: + - supports-color + - typescript + '@typescript-eslint/visitor-keys@5.0.0': dependencies: '@typescript-eslint/types': 5.0.0 @@ -3862,35 +4010,40 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 - '@vitest/expect@2.0.4': + '@typescript-eslint/visitor-keys@8.0.0': dependencies: - '@vitest/spy': 2.0.4 - '@vitest/utils': 2.0.4 + '@typescript-eslint/types': 8.0.0 + eslint-visitor-keys: 3.4.3 + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/pretty-format@2.0.4': + '@vitest/pretty-format@2.0.5': dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@2.0.4': + '@vitest/runner@2.0.5': dependencies: - '@vitest/utils': 2.0.4 + '@vitest/utils': 2.0.5 pathe: 1.1.2 - '@vitest/snapshot@2.0.4': + '@vitest/snapshot@2.0.5': dependencies: - '@vitest/pretty-format': 2.0.4 + '@vitest/pretty-format': 2.0.5 magic-string: 0.30.11 pathe: 1.1.2 - '@vitest/spy@2.0.4': + '@vitest/spy@2.0.5': dependencies: tinyspy: 3.0.0 - '@vitest/utils@2.0.4': + '@vitest/utils@2.0.5': dependencies: - '@vitest/pretty-format': 2.0.4 + '@vitest/pretty-format': 2.0.5 estree-walker: 3.0.3 loupe: 3.1.1 tinyrainbow: 1.2.0 @@ -3901,17 +4054,17 @@ snapshots: '@whatwg-node/fetch@0.9.19': dependencies: - '@whatwg-node/node-fetch': 0.5.19 + '@whatwg-node/node-fetch': 0.5.20 urlpattern-polyfill: 10.0.0 - '@whatwg-node/node-fetch@0.5.19': + '@whatwg-node/node-fetch@0.5.20': dependencies: '@kamilkisiela/fast-url-parser': 1.1.4 busboy: 1.6.0 fast-querystring: 1.1.2 tslib: 2.6.3 - '@whatwg-node/server@0.9.45': + '@whatwg-node/server@0.9.46': dependencies: '@whatwg-node/fetch': 0.9.19 tslib: 2.6.3 @@ -4379,6 +4532,8 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + es-toolkit@1.13.1: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -4416,13 +4571,13 @@ snapshots: dependencies: eslint: 9.8.0 - eslint-config-prisma@0.6.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-deprecation@3.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3(eslint@9.8.0))(eslint-plugin-tsdoc@0.3.0)(eslint@9.8.0)(typescript@5.5.4): + eslint-config-prisma@0.6.0(@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-deprecation@3.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3(eslint@9.8.0))(eslint-plugin-tsdoc@0.3.0)(eslint@9.8.0)(typescript@5.5.4): dependencies: '@eslint/js': 9.8.0 '@types/eslint-config-prettier': 6.11.3 '@types/eslint__js': 8.42.3 - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/parser': 7.18.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) '@typescript-eslint/utils': 7.18.0(eslint@9.8.0)(typescript@5.5.4) eslint: 9.8.0 eslint-config-prettier: 9.1.0(eslint@9.8.0) @@ -4438,7 +4593,7 @@ snapshots: eslint-config-standard@13.0.1(eslint-plugin-import@2.23.3(@typescript-eslint/parser@5.0.0(eslint@9.8.0)(typescript@4.4.3))(eslint-import-resolver-typescript@2.5.0)(eslint@9.8.0))(eslint-plugin-node@11.1.0(eslint@9.8.0))(eslint-plugin-promise@4.2.1)(eslint-plugin-standard@4.0.0(eslint@9.8.0))(eslint@9.8.0): dependencies: eslint: 9.8.0 - eslint-plugin-import: 2.23.3(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) + eslint-plugin-import: 2.23.3(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) eslint-plugin-node: 11.1.0(eslint@9.8.0) eslint-plugin-promise: 4.2.1 eslint-plugin-standard: 4.0.0(eslint@9.8.0) @@ -4455,7 +4610,7 @@ snapshots: dependencies: debug: 4.3.6 eslint: 9.8.0 - eslint-plugin-import: 2.23.3(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) + eslint-plugin-import: 2.23.3(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) glob: 7.2.3 is-glob: 4.0.3 resolve: 1.22.8 @@ -4463,11 +4618,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) eslint: 9.8.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: @@ -4489,7 +4644,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.23.3(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0): + eslint-plugin-import@2.23.3(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0): dependencies: array-includes: 3.1.8 array.prototype.flat: 1.3.2 @@ -4497,7 +4652,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.8.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0) find-up: 2.1.0 has: 1.0.4 is-core-module: 2.15.0 @@ -4508,7 +4663,7 @@ snapshots: resolve: 1.22.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -4590,7 +4745,7 @@ snapshots: eslint-config-prettier: 6.0.0(eslint@9.8.0) eslint-config-standard: 13.0.1(eslint-plugin-import@2.23.3(@typescript-eslint/parser@5.0.0(eslint@9.8.0)(typescript@4.4.3))(eslint-import-resolver-typescript@2.5.0)(eslint@9.8.0))(eslint-plugin-node@11.1.0(eslint@9.8.0))(eslint-plugin-promise@4.2.1)(eslint-plugin-standard@4.0.0(eslint@9.8.0))(eslint@9.8.0) eslint-import-resolver-typescript: 2.5.0(eslint-plugin-import@2.23.3)(eslint@9.8.0) - eslint-plugin-import: 2.23.3(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) + eslint-plugin-import: 2.23.3(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) eslint-plugin-jsdoc: 33.1.1(eslint@9.8.0) eslint-plugin-mocha: 8.1.0(eslint@9.8.0) eslint-plugin-node: 11.1.0(eslint@9.8.0) @@ -4911,7 +5066,7 @@ snapshots: '@graphql-yoga/logger': 2.0.0 '@graphql-yoga/subscription': 5.0.1 '@whatwg-node/fetch': 0.9.19 - '@whatwg-node/server': 0.9.45 + '@whatwg-node/server': 0.9.46 dset: 3.1.3 graphql: 16.9.0 lru-cache: 10.4.3 @@ -5752,26 +5907,26 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.19.1: + rollup@4.19.2: dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.19.1 - '@rollup/rollup-android-arm64': 4.19.1 - '@rollup/rollup-darwin-arm64': 4.19.1 - '@rollup/rollup-darwin-x64': 4.19.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.19.1 - '@rollup/rollup-linux-arm-musleabihf': 4.19.1 - '@rollup/rollup-linux-arm64-gnu': 4.19.1 - '@rollup/rollup-linux-arm64-musl': 4.19.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.19.1 - '@rollup/rollup-linux-riscv64-gnu': 4.19.1 - '@rollup/rollup-linux-s390x-gnu': 4.19.1 - '@rollup/rollup-linux-x64-gnu': 4.19.1 - '@rollup/rollup-linux-x64-musl': 4.19.1 - '@rollup/rollup-win32-arm64-msvc': 4.19.1 - '@rollup/rollup-win32-ia32-msvc': 4.19.1 - '@rollup/rollup-win32-x64-msvc': 4.19.1 + '@rollup/rollup-android-arm-eabi': 4.19.2 + '@rollup/rollup-android-arm64': 4.19.2 + '@rollup/rollup-darwin-arm64': 4.19.2 + '@rollup/rollup-darwin-x64': 4.19.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.19.2 + '@rollup/rollup-linux-arm-musleabihf': 4.19.2 + '@rollup/rollup-linux-arm64-gnu': 4.19.2 + '@rollup/rollup-linux-arm64-musl': 4.19.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.19.2 + '@rollup/rollup-linux-riscv64-gnu': 4.19.2 + '@rollup/rollup-linux-s390x-gnu': 4.19.2 + '@rollup/rollup-linux-x64-gnu': 4.19.2 + '@rollup/rollup-linux-x64-musl': 4.19.2 + '@rollup/rollup-win32-arm64-msvc': 4.19.2 + '@rollup/rollup-win32-ia32-msvc': 4.19.2 + '@rollup/rollup-win32-x64-msvc': 4.19.2 fsevents: 2.3.3 rrweb-cssom@0.6.0: {} @@ -6031,7 +6186,7 @@ snapshots: tslib: 1.14.1 typescript: 4.4.3 - tsx@4.16.2: + tsx@4.16.5: dependencies: esbuild: 0.21.5 get-tsconfig: 4.7.6 @@ -6102,6 +6257,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.0.0(eslint@9.8.0)(typescript@5.5.4): + dependencies: + '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + typescript@4.4.3: {} typescript@5.3.3: {} @@ -6192,13 +6358,13 @@ snapshots: unist-util-stringify-position: 2.0.3 vfile-message: 2.0.4 - vite-node@2.0.4(@types/node@22.0.0): + vite-node@2.0.5(@types/node@22.0.2): dependencies: cac: 6.7.14 debug: 4.3.6 pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.3.5(@types/node@22.0.0) + vite: 5.3.5(@types/node@22.0.2) transitivePeerDependencies: - '@types/node' - less @@ -6209,24 +6375,24 @@ snapshots: - supports-color - terser - vite@5.3.5(@types/node@22.0.0): + vite@5.3.5(@types/node@22.0.2): dependencies: esbuild: 0.21.5 postcss: 8.4.40 - rollup: 4.19.1 + rollup: 4.19.2 optionalDependencies: - '@types/node': 22.0.0 + '@types/node': 22.0.2 fsevents: 2.3.3 - vitest@2.0.4(@types/node@22.0.0)(happy-dom@14.12.3)(jsdom@24.1.1): + vitest@2.0.5(@types/node@22.0.2)(happy-dom@14.12.3)(jsdom@24.1.1): dependencies: '@ampproject/remapping': 2.3.0 - '@vitest/expect': 2.0.4 - '@vitest/pretty-format': 2.0.4 - '@vitest/runner': 2.0.4 - '@vitest/snapshot': 2.0.4 - '@vitest/spy': 2.0.4 - '@vitest/utils': 2.0.4 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 chai: 5.1.1 debug: 4.3.6 execa: 8.0.1 @@ -6236,11 +6402,11 @@ snapshots: tinybench: 2.8.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.3.5(@types/node@22.0.0) - vite-node: 2.0.4(@types/node@22.0.0) + vite: 5.3.5(@types/node@22.0.2) + vite-node: 2.0.5(@types/node@22.0.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.0.0 + '@types/node': 22.0.2 happy-dom: 14.12.3 jsdom: 24.1.1 transitivePeerDependencies: diff --git a/src/entrypoints/alpha/client.ts b/src/entrypoints/alpha/client.ts index 915dc00cf..eaad3901e 100644 --- a/src/entrypoints/alpha/client.ts +++ b/src/entrypoints/alpha/client.ts @@ -1,2 +1,3 @@ export { create as createSelect, select } from '../../layers/5_select/select.js' -export { Client, create, createPrefilled, Input, InputPrefilled } from '../../layers/6_client/client.js' +export { Client, create, createPrefilled } from '../../layers/6_client/client.js' +export { Input, InputPrefilled } from '../../layers/6_client/Settings/Input.js' diff --git a/src/layers/0_functions/request.ts b/src/layers/0_functions/request.ts index 2a6c5169f..431d57ef7 100644 --- a/src/layers/0_functions/request.ts +++ b/src/layers/0_functions/request.ts @@ -43,6 +43,7 @@ export const request: NetworkRequest = async (input) => { } const json = await response.json() as object + const result = parseExecutionResult(json) return result diff --git a/src/layers/3_SelectionSet/encode.test.ts b/src/layers/3_SelectionSet/encode.test.ts index 42630f435..165017a5f 100644 --- a/src/layers/3_SelectionSet/encode.test.ts +++ b/src/layers/3_SelectionSet/encode.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'vitest' import { db } from '../../../tests/_/db.js' import type { Index } from '../../../tests/_/schema/generated/Index.js' import { $Index as schemaIndex } from '../../../tests/_/schema/generated/SchemaRuntime.js' +import { outputConfigDefault } from '../6_client/Settings/Config.js' import type { SelectionSet } from './__.js' import type { Context } from './encode.js' import { rootTypeSelectionSet } from './encode.js' @@ -21,7 +22,7 @@ const testEachArgs = [ ] ) => { const [description, ss] = args.length === 1 ? [undefined, args[0]] : args - const context: Context = { schemaIndex, config: { returnMode: `data` } } + const context: Context = { schemaIndex, config: { output: outputConfigDefault, transport: `memory` } } const graphqlDocumentString = rootTypeSelectionSet(context, schemaIndex[`Root`][`Query`], ss as any) // Should parse, ensures is syntactically valid graphql document. const document = parse(graphqlDocumentString) diff --git a/src/layers/3_SelectionSet/encode.ts b/src/layers/3_SelectionSet/encode.ts index 9455d42a8..06c528f80 100644 --- a/src/layers/3_SelectionSet/encode.ts +++ b/src/layers/3_SelectionSet/encode.ts @@ -2,7 +2,7 @@ import { RootTypeName } from '../../lib/graphql.js' import { assertArray, assertObject, lowerCaseFirstLetter } from '../../lib/prelude.js' import { Schema } from '../1_Schema/__.js' import { readMaybeThunk } from '../1_Schema/core/helpers.js' -import type { ReturnModeType } from '../6_client/Config.js' +import type { Config } from '../6_client/Settings/Config.js' import type { SelectionSet } from './__.js' import { isSelectFieldName } from './helpers.js' import { parseClientDirectiveDefer } from './runtime/directives/defer.js' @@ -44,9 +44,7 @@ type FieldValue = SS | Indicator export interface Context { schemaIndex: Schema.Index - config: { - returnMode: ReturnModeType - } + config: Config } export const rootTypeSelectionSet = ( @@ -200,7 +198,7 @@ export const resolveObjectLikeFieldValue = ( * Inject __typename field for result fields that are missing it. */ // dprint-ignore - if (rootTypeName && context.config.returnMode === `dataSuccess` && context.schemaIndex.error.rootResultFields[rootTypeName][fieldName.actual]) { + if (rootTypeName && context.config.output.errors.schema !== false && context.schemaIndex.error.rootResultFields[rootTypeName][fieldName.actual]) { (ss as Record)[`__typename`] = true } return `${toGraphQLFieldName(fieldName)} ${resolveFieldValue(context, schemaField, ss)}` diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index c9a163ca8..e4b433658 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -60,6 +60,9 @@ export type HookDefEncode = { > & TransportInput<{ schema: string | URL }, { schema: GraphQLSchema }> slots: { + /** + * Create the value that will be used as the HTTP body for the sent GraphQL request. + */ body: ( input: { query: string; variables?: StandardScalarVariables; operationName?: string }, ) => BodyInit @@ -137,6 +140,9 @@ export type HookDefDecode = { input: & { result: ExecutionResult } & InterfaceInput + & TransportInput< + { response: Response } + > } export type HookMap = { @@ -309,14 +315,36 @@ export const anyware = Anyware.create({ switch (input.interface) { // todo this depends on the return mode case `raw`: { - return input.result + switch (input.transport) { + case `http`: { + return { + ...input.result, + response: input.response, + } + } + case `memory`: { + return input.result + } + default: + throw casesExhausted(input) + } } case `typed`: { // 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 const dataDecoded = Result.decode(getRootIndexOrThrow(input.context, input.rootTypeName), input.result.data) - return { ...input.result, data: dataDecoded } + // console.log(8, Object.keys({ ...input.result, data: dataDecoded })) + switch (input.transport) { + case `memory`: { + return { ...input.result, data: dataDecoded } + } + case `http`: { + return { ...input.result, data: dataDecoded, response: input.response } + } + default: + throw casesExhausted(input) + } } default: throw casesExhausted(input) diff --git a/src/layers/5_core/types.ts b/src/layers/5_core/types.ts index 22f360ad5..ffd32dc61 100644 --- a/src/layers/5_core/types.ts +++ b/src/layers/5_core/types.ts @@ -1,11 +1,16 @@ import type { Schema } from '../1_Schema/__.js' -import type { Config } from '../6_client/Config.js' +import type { Config } from '../6_client/Settings/Config.js' export type Transport = TransportMemory | TransportHttp -export type TransportMemory = 'memory' +export type TransportMemory = typeof Transport.memory -export type TransportHttp = 'http' +export type TransportHttp = typeof Transport.http + +export const Transport = { + memory: `memory`, + http: `http`, +} as const export type Interface = InterfaceRaw | InterfaceTyped diff --git a/src/layers/6_client/Config.ts b/src/layers/6_client/Config.ts deleted file mode 100644 index 1891a4cb3..000000000 --- a/src/layers/6_client/Config.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { ExecutionResult } from 'graphql' -import type { GraphQLExecutionResultError } from '../../lib/graphql.js' -import type { SetProperty, StringKeyof } from '../../lib/prelude.js' -import type { Schema } from '../1_Schema/__.js' -import type { GlobalRegistry } from '../2_generator/globalRegistry.js' -import type { SelectionSet } from '../3_SelectionSet/__.js' - -export type ReturnModeType = - | ReturnModeTypeGraphQL - | ReturnModeTypeGraphQLSuccess - | ReturnModeTypeDataSuccess - | ReturnModeTypeData - | ReturnModeTypeDataAndErrors - -export type ReturnModeTypeBase = - | ReturnModeTypeGraphQLSuccess - | ReturnModeTypeGraphQL - | ReturnModeTypeDataAndErrors - | ReturnModeTypeData - -export type ReturnModeTypeGraphQLSuccess = 'graphqlSuccess' - -export type ReturnModeTypeGraphQL = 'graphql' - -export type ReturnModeTypeData = 'data' - -export type ReturnModeTypeDataAndErrors = 'dataAndErrors' - -export type ReturnModeTypeDataSuccess = 'dataSuccess' - -export type OptionsInput = { - returnMode: ReturnModeType | undefined -} - -export type OptionsInputDefaults = { - returnMode: 'data' -} - -export type Config = { - returnMode: ReturnModeType -} - -export type ApplyInputDefaults = { - [Key in keyof OptionsInputDefaults]: undefined extends Input[Key] ? OptionsInputDefaults[Key] - : Exclude -} - -// dprint-ignore -export type ReturnModeRootType<$Config extends Config, $Index extends Schema.Index, $Data extends object> = - $Config['returnMode'] extends 'graphql' ? ExecutionResult<$Data> : - $Config['returnMode'] extends 'data' ? $Data : - $Config['returnMode'] extends 'dataSuccess' ? { [$Key in keyof $Data]: ExcludeSchemaErrors<$Index, $Data[$Key]> } : - $Data | GraphQLExecutionResultError - -// dprint-ignore -export type ReturnModeRootField<$Config extends Config, $Index extends Schema.Index, $Data, $DataRaw = undefined> = - $Config['returnMode'] extends 'graphql' ? ExecutionResult<$DataRaw extends undefined ? $Data : $DataRaw> : - $Config['returnMode'] extends 'data' ? $Data : - $Config['returnMode'] extends 'dataSuccess' ? ExcludeSchemaErrors<$Index, $Data> : - $Data | GraphQLExecutionResultError - -export type ExcludeSchemaErrors<$Index extends Schema.Index, $Data> = Exclude< - $Data, - $Index['error']['objectsTypename'][keyof $Index['error']['objectsTypename']] -> - -export type OrThrowifyConfig<$Config extends Config> = $Config['returnMode'] extends 'graphql' ? $Config - : SetProperty<$Config, 'returnMode', 'dataSuccess'> - -/** - * We inject __typename select when: - * 1. using schema errors - * 2. using return mode dataSuccess - */ - -type TypenameSelection = { __typename: true } - -// dprint-ignore -export type CreateSelectionTypename<$Config extends Config, $Index extends Schema.Index> = - IsNeedSelectionTypename<$Config, $Index> extends true ? TypenameSelection : {} // eslint-disable-line - -// dprint-ignore -export type IsNeedSelectionTypename<$Config extends Config, $Index extends Schema.Index> = - $Config['returnMode'] extends 'dataSuccess' ? GlobalRegistry.HasSchemaErrorsViaName<$Index['name']> extends true ? true : - false : - false - -export type AugmentRootTypeSelectionWithTypename< - $Config extends Config, - $Index extends Schema.Index, - $RootTypeName extends Schema.RootTypeName, - $Selection extends object, -> = IsNeedSelectionTypename<$Config, $Index> extends true ? { - [$Key in StringKeyof<$Selection>]: - & $Selection[$Key] - & (IsRootFieldNameAResultField<$Index, $RootTypeName, $Key> extends true ? TypenameSelection : {}) // eslint-disable-line - } - : $Selection - -type IsRootFieldNameAResultField< - $Index extends Schema.Index, - $RootTypeName extends Schema.RootTypeName, - $FieldName extends string, -> = SelectionSet.AliasNameOrigin<$FieldName> extends keyof $Index['error']['rootResultFields'][$RootTypeName] ? true - : false diff --git a/src/layers/6_client/RootTypeMethods.ts b/src/layers/6_client/RootTypeMethods.ts index 33f7a9c7a..2fd8db28b 100644 --- a/src/layers/6_client/RootTypeMethods.ts +++ b/src/layers/6_client/RootTypeMethods.ts @@ -9,9 +9,9 @@ import type { Config, CreateSelectionTypename, OrThrowifyConfig, - ReturnModeRootField, - ReturnModeRootType, -} from './Config.js' + ResolveOutputReturnRootField, + ResolveOutputReturnRootType, +} from './Settings/Config.js' type RootTypeFieldContext = { Config: Config @@ -27,12 +27,15 @@ export type GetRootTypeMethods<$Config extends Config, $Index extends Schema.Ind RootTypeMethods<$Config, $Index, Capitalize<$OperationName>> } +// type x = OrThrowifyConfig<{ output: OutputConfigDefault; transport: 'http' }> + // dprint-ignore export type RootTypeMethods<$Config extends Config, $Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> = $Index['Root'][$RootTypeName] extends Schema.Object$2 ? ( & { $batch: RootMethod<$Config, $Index, $RootTypeName> + // @ts-expect-error fixme $batchOrThrow: RootMethod, $Index, $RootTypeName> } & { @@ -47,6 +50,7 @@ export type RootTypeMethods<$Config extends Config, $Index extends Schema.Index, } & { [$RootTypeFieldName in keyof $Index['Root'][$RootTypeName]['fields'] & string as `${$RootTypeFieldName}OrThrow`]: + // @ts-expect-error fixme RootTypeFieldMethod<{ Config: OrThrowifyConfig<$Config>, Index: $Index, @@ -61,7 +65,7 @@ export type RootTypeMethods<$Config extends Config, $Index extends Schema.Index, // dprint-ignore type RootMethod<$Config extends Config, $Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> = <$SelectionSet extends object>(selectionSet: Exact<$SelectionSet, SelectionSet.Root<$Index, $RootTypeName>>) => - Promise, $Index, $RootTypeName>>> + Promise, $Index, $RootTypeName>>> // dprint-ignore type RootTypeFieldMethod<$Context extends RootTypeFieldContext> = @@ -88,4 +92,4 @@ type ScalarFieldMethod<$Context extends RootTypeFieldContext> = (() => Promise>>) // dprint-ignore type ReturnModeForFieldMethod<$Context extends RootTypeFieldContext, $Data> = - ReturnModeRootField<$Context['Config'], $Context['Index'], $Data, { [k in $Context['RootTypeFieldName']] : $Data }> + ResolveOutputReturnRootField<$Context['Config'], $Context['Index'], $Data, { [k in $Context['RootTypeFieldName']] : $Data }> diff --git a/src/layers/6_client/Settings/Config.ts b/src/layers/6_client/Settings/Config.ts new file mode 100644 index 000000000..08f9360bb --- /dev/null +++ b/src/layers/6_client/Settings/Config.ts @@ -0,0 +1,232 @@ +import type { ExecutionResult as GraphQLExecutionResult } from 'graphql' +import type { Simplify } from 'type-fest' +import type { GraphQLExecutionResultError } from '../../../lib/graphql.js' +import type { ConfigManager, StringKeyof } from '../../../lib/prelude.js' +import type { Schema } from '../../1_Schema/__.js' +import type { GlobalRegistry } from '../../2_generator/globalRegistry.js' +import type { SelectionSet } from '../../3_SelectionSet/__.js' +import type { Transport } from '../../5_core/types.js' + +export type OutputChannel = 'throw' | 'return' + +export type OutputChannelConfig = 'throw' | 'return' | 'default' + +export type ErrorCategory = 'execution' | 'other' | 'schema' + +export const readConfigErrorCategoryOutputChannel = ( + config: Config, + errorCategory: ErrorCategory, +): OutputChannel | false => { + if (config.output.errors[errorCategory] === `default`) { + return config.output.defaults.errorChannel + } + return config.output.errors[errorCategory] +} + +export const traditionalGraphqlOutput: OutputConfig = { + defaults: { errorChannel: `throw` }, + envelope: { enabled: true, errors: { execution: true, other: false, schema: false } }, + errors: { execution: `default`, other: `default`, schema: false }, +} +export const traditionalGraphqlOutputThrowing: OutputConfig = { + ...traditionalGraphqlOutput, + envelope: { + ...traditionalGraphqlOutput.envelope, + errors: { + ...traditionalGraphqlOutput.envelope.errors, + execution: false, + }, + }, +} + +export const isContextConfigTraditionalGraphQLOutput = (config: Config) => { + return config.output.envelope.enabled && config.output.envelope.errors.execution + && !config.output.envelope.errors.other && !config.output.envelope.errors.schema +} + +export type OutputConfig = { + defaults: { + errorChannel: OutputChannel + } + envelope: { + enabled: boolean + errors: { + execution: boolean + other: boolean + schema: boolean + } + } + errors: { + execution: OutputChannelConfig + other: OutputChannelConfig + schema: false | OutputChannelConfig + } +} + +export const outputConfigDefault: OutputConfigDefault = { + defaults: { + errorChannel: `throw`, + }, + envelope: { + enabled: false, + errors: { + execution: true, + other: false, + schema: false, + }, + }, + errors: { + execution: `default`, + other: `default`, + schema: false, + }, +} + +export type OutputConfigDefault = { + defaults: { + errorChannel: 'throw' + } + envelope: { + enabled: false + errors: { + execution: true + other: false + schema: false + } + } + errors: { + execution: 'default' + other: 'default' + schema: false + } +} + +export type Config = { + output: OutputConfig + transport: Transport +} + +// dprint-ignore +export type ResolveOutputReturnRootType<$Config extends Config, $Index extends Schema.Index, $Data> = + | IfConfiguredGetOutputErrorReturns<$Config> + | ( + $Config['output']['envelope']['enabled'] extends true + ? Envelope<$Config, IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $Data>> + : Simplify> + ) + +// dprint-ignore +export type ResolveOutputReturnRootField<$Config extends Config, $Index extends Schema.Index, $Data, $DataRaw = undefined> = + | IfConfiguredGetOutputErrorReturns<$Config> + | ( + $Config['output']['envelope']['enabled'] extends true + // todo: a typed execution result that allows for additional error types. + // currently it is always graphql execution error however envelope configuration can put more errors into that. + ? Envelope<$Config, $DataRaw extends undefined + ? IfConfiguredStripSchemaErrorsFromDataRootField<$Config, $Index, $Data> + : IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $DataRaw>> + : Simplify> + ) + +// type ObjMap = { +// [key: string]: T +// } + +// dprint-ignore +// todo use ObjMap +export type Envelope<$Config extends Config, $Data = unknown> = + Simplify< + $Config['transport'] extends 'http' + ? EnvelopeTransportHttp<$Data> + : EnvelopeTransportMemory<$Data> + > + +export type EnvelopeTransportHttp<$Data> = GraphQLExecutionResult<$Data> & { + response: Response +} + +export type EnvelopeTransportMemory<$Data> = GraphQLExecutionResult<$Data> + +type ConfigResolveOutputErrorChannel<$Config extends Config, $Channel extends OutputChannelConfig | false> = + $Channel extends 'default' ? $Config['output']['defaults']['errorChannel'] + : $Channel extends false ? false + : $Channel + +// dprint-ignore +type ConfigGetOutputEnvelopeErrorChannel<$Config extends Config, $ErrorCategory extends ErrorCategory> = + $Config['output']['envelope']['errors'][$ErrorCategory] extends true + ? false + : ConfigResolveOutputErrorChannel<$Config, $Config['output']['errors'][$ErrorCategory]> + +// dprint-ignore +type ConfigGetOutputError<$Config extends Config, $ErrorCategory extends ErrorCategory> = + $Config['output']['envelope']['enabled'] extends true + ? ConfigGetOutputEnvelopeErrorChannel<$Config, $ErrorCategory> + : ConfigResolveOutputErrorChannel<$Config, $Config['output']['errors'][$ErrorCategory]> + +// dprint-ignore +type IfConfiguredGetOutputErrorReturns<$Config extends Config> = + | (ConfigGetOutputError<$Config, 'execution'> extends 'return' ? GraphQLExecutionResultError : never) + | (ConfigGetOutputError<$Config, 'other'> extends 'return' ? Error : never) + | (ConfigGetOutputError<$Config, 'schema'> extends 'return' ? Error : never) + +// dprint-ignore +type IfConfiguredStripSchemaErrorsFromDataRootType<$Config extends Config, $Index extends Schema.Index, $Data> = + { [$RootFieldName in keyof $Data]: IfConfiguredStripSchemaErrorsFromDataRootField<$Config, $Index, $Data[$RootFieldName]> } + +// dprint-ignore +type IfConfiguredStripSchemaErrorsFromDataRootField<$Config extends Config, $Index extends Schema.Index, $Data> = + $Config['output']['errors']['schema'] extends false + ? $Data + : ExcludeSchemaErrors<$Index, $Data> + +// dprint-ignore +export type ExcludeSchemaErrors<$Index extends Schema.Index, $Data> = + Exclude< + $Data, + $Index['error']['objectsTypename'][keyof $Index['error']['objectsTypename']] + > + +// todo this changed, check tests, add new tests as needed. +// dprint-ignore +export type OrThrowifyConfig<$Config extends Config> = + ConfigManager.Set<$Config, ['output', 'errors'], { other: 'throw', execution: 'throw', schema: 'throw' }> + +/** + * We inject __typename select when: + * 1. using schema errors + * 2. using return mode dataSuccess + */ + +type TypenameSelection = { __typename: true } + +// dprint-ignore +export type CreateSelectionTypename<$Config extends Config, $Index extends Schema.Index> = + IsNeedSelectionTypename<$Config, $Index> extends true ? TypenameSelection : {} // eslint-disable-line + +// dprint-ignore +export type IsNeedSelectionTypename<$Config extends Config, $Index extends Schema.Index> = + ConfigGetOutputError<$Config, 'schema'> extends 'throw' + ? GlobalRegistry.HasSchemaErrorsViaName<$Index['name']> extends true + ? true + : false + : false + +export type AugmentRootTypeSelectionWithTypename< + $Config extends Config, + $Index extends Schema.Index, + $RootTypeName extends Schema.RootTypeName, + $Selection extends object, +> = IsNeedSelectionTypename<$Config, $Index> extends true ? { + [$Key in StringKeyof<$Selection>]: + & $Selection[$Key] + & (IsRootFieldNameAResultField<$Index, $RootTypeName, $Key> extends true ? TypenameSelection : {}) // eslint-disable-line + } + : $Selection + +type IsRootFieldNameAResultField< + $Index extends Schema.Index, + $RootTypeName extends Schema.RootTypeName, + $FieldName extends string, +> = SelectionSet.AliasNameOrigin<$FieldName> extends keyof $Index['error']['rootResultFields'][$RootTypeName] ? true + : false diff --git a/src/layers/6_client/Settings/Input.ts b/src/layers/6_client/Settings/Input.ts new file mode 100644 index 000000000..019269639 --- /dev/null +++ b/src/layers/6_client/Settings/Input.ts @@ -0,0 +1,184 @@ +import type { GraphQLSchema } from 'graphql' +import type { ConfigManager } from '../../../lib/prelude.js' +import type { URLInput } from '../../0_functions/request.js' +import type { Schema } from '../../1_Schema/__.js' +import type { GlobalRegistry } from '../../2_generator/globalRegistry.js' +import type { TransportHttp, TransportMemory } from '../../5_core/types.js' +import { Transport } from '../../5_core/types.js' +import { type OutputChannel, type OutputChannelConfig, outputConfigDefault } from './Config.js' + +export type InputOutputEnvelopeLonghand = { + /** + * @defaultValue `true` + */ + enabled?: boolean + errors?: { + execution?: boolean + other?: boolean + } +} + +export type Input<$Schema extends GlobalRegistry.SchemaList> = { + /** + * Used internally. + * + * When custom scalars are being used, this runtime schema is used to + * encode/decode them before/after your application sends/receives them. + * + * When using root type field methods, this runtime schema is used to assist how arguments on scalars versus objects + * are constructed into the sent GraphQL document. + */ + readonly schemaIndex?: Schema.Index | null + /** + * The schema to use. + * + * TODO why don't we infer this from the runtime schemaIndex? + * + * @defaultValue 'default' + */ + name?: $Schema['index']['name'] + // todo way to hide Relay input pattern of nested input + // elideInputKey: true, +} & InputPrefilled<$Schema> + +export type InputRaw<$Schema extends GlobalRegistry.SchemaList> = { + schema: URLInput + /** + * Headers to send with the request. + */ + headers?: HeadersInit + /** + * Configure output behavior, such as if errors should be returned or thrown. + */ + output?: OutputInput<{ schemaErrors: GlobalRegistry.HasSchemaErrors<$Schema>; transport: 'http' }> +} | { + schema: GraphQLSchema + headers?: never + /** + * Configure output behavior, such as if errors should be returned or thrown. + */ + output?: OutputInput<{ schemaErrors: GlobalRegistry.HasSchemaErrors<$Schema>; transport: 'memory' }> +} + +export type OutputInput = + & { + /** + * Defaults for certain aspects of output behavior. + */ + defaults?: { + /** + * The default error channel to use. + * + * @defaultValue `'throw'` + */ + errorChannel?: OutputChannel + } + /** + * @defaultValue `false` + */ + envelope?: boolean | InputOutputEnvelopeLonghand + /** + * Granular control of how to output errors by category. + */ + errors?: { + /** + * Execution errors. These are errors you would traditionally see in the GraphQL execution result `'errors'` field. + */ + execution?: OutputChannelConfig + /** + * Other errors include things like network errors thrown by fetch (when using HTTP transport), errors thrown from extensions, etc. + */ + other?: OutputChannelConfig + } + } + // & (Options['transport'] extends 'http' ? { + // response?: boolean + // } + // : {}) // eslint-disable-line + & (Options['schemaErrors'] extends true ? { + envelope?: { + errors?: { + schema?: boolean + } + } + errors?: { + schema?: false | OutputChannelConfig + } + } + : {}) // eslint-disable-line + +// dprint-ignore +export type InputToConfig<$Input extends Input> = { + transport: InferTransport<$Input> + output: { + defaults: { + errorChannel: ConfigManager.ReadOrDefault<$Input, ['output', 'defaults', 'errorChannel'], 'throw'> + } + envelope: { + enabled: + ConfigManager.Read<$Input, ['output','envelope']> extends boolean ? ConfigManager.Read<$Input, ['output','envelope']> + : ConfigManager.Read<$Input, ['output','envelope','enabled']> extends boolean ? ConfigManager.Read<$Input, ['output','envelope','enabled']> + : ConfigManager.Read<$Input, ['output','envelope']> extends object ? true + : false + errors: { + execution: ConfigManager.ReadOrDefault<$Input, ['output','envelope','errors','execution'], true> + other: ConfigManager.ReadOrDefault<$Input, ['output','envelope','errors','other'], false> + schema: ConfigManager.ReadOrDefault<$Input, ['output','envelope','errors','schema'], false> + } + } + errors: { + execution: ConfigManager.ReadOrDefault<$Input,['output', 'errors', 'execution'], 'default'> + other: ConfigManager.ReadOrDefault<$Input,['output', 'errors', 'other'], 'default'> + schema: ConfigManager.ReadOrDefault<$Input,['output', 'errors', 'schema'], false> + } + } +} + +export const inputToConfig = >(input: T): InputToConfig => { + const envelopeLonghand: InputOutputEnvelopeLonghand | undefined = typeof input.output?.envelope === `object` + ? { enabled: true, ...input.output.envelope } + : typeof input.output?.envelope === `boolean` + ? { enabled: input.output.envelope } + : undefined + return { + transport: inferTransport(input), + output: { + defaults: { + // @ts-expect-error conditional type + errorChannel: input.output?.defaults?.errorChannel ?? outputConfigDefault.defaults.errorChannel, + }, + envelope: { + // @ts-expect-error conditional type + enabled: envelopeLonghand?.enabled ?? outputConfigDefault.envelope.enabled, + errors: { + // @ts-expect-error conditional type + execution: envelopeLonghand?.errors?.execution ?? outputConfigDefault.envelope.errors.execution, + // @ts-expect-error conditional type + other: envelopeLonghand?.errors?.other ?? outputConfigDefault.envelope.errors.other, + // @ts-expect-error conditional type + // eslint-disable-next-line + schema: envelopeLonghand?.errors?.schema ?? outputConfigDefault.envelope.errors.schema, + }, + }, + errors: { + // @ts-expect-error conditional type + execution: input.output?.errors?.execution ?? outputConfigDefault.errors.execution, + // @ts-expect-error conditional type + other: input.output?.errors?.other ?? outputConfigDefault.errors.other, + // @ts-expect-error conditional type + // eslint-disable-next-line + schema: input.output?.errors?.schema ?? outputConfigDefault.errors.schema, + }, + }, + } +} + +type InferTransport<$Input extends Input> = $Input['schema'] extends URLInput ? TransportHttp : TransportMemory + +const inferTransport = >(input: T): InferTransport => { + // @ts-expect-error conditional type + return input.schema instanceof URL || typeof input.schema === `string` ? Transport.http : Transport.memory +} + +export type InputPrefilled<$Schema extends GlobalRegistry.SchemaList> = $Schema extends any ? (InputRaw<$Schema>) + : never diff --git a/src/layers/6_client/Settings/client.create.config.output.test-d.ts b/src/layers/6_client/Settings/client.create.config.output.test-d.ts new file mode 100644 index 000000000..ac50420b2 --- /dev/null +++ b/src/layers/6_client/Settings/client.create.config.output.test-d.ts @@ -0,0 +1,181 @@ +/* eslint-disable */ +import { ExecutionResult } from 'graphql' +import { describe } from 'node:test' +import { expectTypeOf, test } from 'vitest' +import { Graffle } from '../../../../tests/_/schema/generated/__.js' +import { schema } from '../../../../tests/_/schema/schema.js' +import { GraphQLExecutionResultError } from '../../../lib/graphql.js' +import { SimplifyDeep } from '../../../lib/prelude.js' +import { EnvelopeTransportMemory } from './Config.js' + +const C = Graffle.create + +const defaultGraffle = Graffle.create({ schema }) + +const resultFieldSelect = + Graffle.Select.Query({ resultNonNull: { $: { case: 'Object1' }, __typename: true } })['resultNonNull'] + +describe('default is errors thrown, no envelope, no schema errors', async () => { + const graffle = C({ + schema, + output: { + defaults: { + errorChannel: 'throw', + }, + envelope: { + enabled: false, + errors: { + execution: true, + other: false, + }, + }, + errors: { + execution: 'default', + other: 'default', + schema: false, + }, + }, + }) + const result1 = await graffle.query.__typename() + const result2 = await defaultGraffle.query.__typename() + expectTypeOf(result1).toEqualTypeOf<'Query'>() + expectTypeOf(result2).toEqualTypeOf<'Query'>() +}) + +// dprint-ignore +describe('.envelope', () => { + type FieldMethodResultDisabled = 'Query' + type FieldMethodResultEnabled = ExecutionResult<{ __typename: FieldMethodResultDisabled }> + + type ResultFieldMethodResultDisabled = { + __typename: 'Object1' + } | { + __typename: 'ErrorOne' + } | { + __typename: 'ErrorTwo' + } + type ResultFieldMethodResultEnabled = ExecutionResult<{ resultNonNull: ResultFieldMethodResultDisabled }> + + // todo reference to Graffle client + // const fieldMethod = <$Graffle extends {query:{__typename:()=>Promise}}>(g: $Graffle) => g.query.__typename() + + describe('false disables it ', () => { + const g = C({ schema, output: { envelope: false } }) + + test('query.', () => { + expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf() + }) + test('query.', () => { + expectTypeOf(g.query.resultNonNull(resultFieldSelect)).resolves.toEqualTypeOf() + }) + test('query.$batch', () => { + expectTypeOf(g.query.$batch({ __typename: true, idNonNull: true })).resolves.toEqualTypeOf<{ __typename: 'Query'; idNonNull: string }>() + }) + }) + describe('true enables it', () => { + const g = Graffle.create({ schema, output: { envelope: true } }) + test('query.', async() => { + expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf() + }) + test('query.', () => { + expectTypeOf(g.query.resultNonNull(resultFieldSelect)).resolves.toEqualTypeOf() + }) + test('query.$batch', () => { + expectTypeOf(g.query.$batch({ __typename: true, idNonNull: true })).resolves.toEqualTypeOf>() + }) + }) + test('object enables it', async () => { + const graffle = Graffle.create({ schema, output: { envelope: {} } }) + expectTypeOf(await graffle.query.__typename()).toEqualTypeOf() + }) + describe('.enabled', () => { + test('false disables it', async () => { + const graffle = Graffle.create({ schema, output: { envelope: { enabled: false } } }) + expectTypeOf(await graffle.query.__typename()).toEqualTypeOf() + }) + test('true enables it', async () => { + const graffle = Graffle.create({ schema, output: { envelope: { enabled: true } } }) + expectTypeOf(await graffle.query.__typename()).toEqualTypeOf() + }) + }) + describe('with defaults.errorChannel: "return"', () => { + describe('.errors', () => { + test('defaults to execution errors in envelope', () => { + const g = C({ schema, output: { defaults: { errorChannel: 'return' }, envelope: true } }) + expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf | Error>() + }) + test('.execution:false restores errors to return', () => { + const g = C({ + schema, + output: { defaults: { errorChannel: 'return' }, envelope: { errors: { execution: false } } }, + }) + expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf< + ExecutionResult<{ __typename: 'Query' }> | Error | GraphQLExecutionResultError + >() + }) + test('.other:true raises them to envelope', () => { + const g = C({ + schema, + output: { defaults: { errorChannel: 'return' }, envelope: { errors: { other: true } } }, + }) + expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf>() + }) + }) + }) +}) + +describe('defaults.errorChannel: "return"', () => { + describe('puts errors into return type', () => { + const g = C({ schema, output: { defaults: { errorChannel: 'return' } } }) + test('query.', () => { + expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf<'Query' | Error | GraphQLExecutionResultError>() + }) + }) + describe('with .errors', () => { + test('.execution: throw', async () => { + const g = C({ + schema, + output: { defaults: { errorChannel: 'return' }, errors: { execution: 'throw' } }, + }) + expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | Error>() + }) + test('.other: throw', async () => { + const g = C({ + schema, + output: { defaults: { errorChannel: 'return' }, errors: { other: 'throw' } }, + }) + expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | GraphQLExecutionResultError>() + }) + test('.*: throw', async () => { + const g = C({ + schema, + output: { defaults: { errorChannel: 'return' }, errors: { other: 'throw', execution: 'throw' } }, + }) + expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query'>() + }) + }) +}) + +describe('.errors.schema', () => { + describe('throw', () => { + const g = C({ schema, output: { errors: { schema: 'throw' } } }) + test('query.', () => { + expectTypeOf(g.query.resultNonNull(resultFieldSelect)).resolves.toEqualTypeOf<{ __typename: 'Object1' }>() + }) + }) + describe('return', () => { + const g = C({ schema, output: { errors: { schema: 'return' } } }) + test('query.', () => { + expectTypeOf(g.query.resultNonNull(resultFieldSelect)).resolves.toEqualTypeOf<{ __typename: 'Object1' } | Error>() + }) + }) + describe('envelope.schema', () => { + const g = C({ schema, output: { envelope: { errors: { schema: true } }, errors: { schema: 'return' } } }) + test('query.', async () => { + // todo: once we have execution result with type variable errors, then enahnce this test to assert that the result errors come through in the errors field. + expectTypeOf(g.query.resultNonNull(resultFieldSelect)).resolves.toEqualTypeOf< + EnvelopeTransportMemory<{ resultNonNull: { __typename: 'Object1' } }> + >() + }) + }) +}) diff --git a/src/layers/6_client/Settings/client.input.test-d.ts b/src/layers/6_client/Settings/client.input.test-d.ts new file mode 100644 index 000000000..fd862684e --- /dev/null +++ b/src/layers/6_client/Settings/client.input.test-d.ts @@ -0,0 +1,10 @@ +import { test } from 'vitest' +import { Graffle } from '../../../../tests/_/schema/generated/__.js' +import { schema } from '../../../../tests/_/schema/schema.js' +import { QueryOnly } from '../../../../tests/_/schemaQueryOnly/generated/__.js' + +test(`works`, () => { + Graffle.create({ schema, output: { errors: { schema: `throw` } } }) + // @ts-expect-error schema error config not available. + QueryOnly.create({ schema, name: `QueryOnly`, output: { errors: { schema: `throw` } } }) +}) diff --git a/src/layers/6_client/client.extend.test.ts b/src/layers/6_client/client.extend.test.ts index 06d2d5672..444749813 100644 --- a/src/layers/6_client/client.extend.test.ts +++ b/src/layers/6_client/client.extend.test.ts @@ -5,7 +5,7 @@ import { createResponse, test } from '../../../tests/_/helpers.js' import { Graffle } from '../../../tests/_/schema/generated/__.js' import { oops } from '../../lib/anyware/specHelpers.js' -const client = Graffle.create({ schema: 'https://foo', returnMode: 'dataAndErrors' }) +const client = Graffle.create({ schema: 'https://foo', output: { defaults: { errorChannel: 'return' } } }) const headers = { 'x-foo': 'bar' } test('using an extension returns a copy of the client', () => { diff --git a/src/layers/6_client/client.input.test-d.ts b/src/layers/6_client/client.input.test-d.ts deleted file mode 100644 index 5a121b65d..000000000 --- a/src/layers/6_client/client.input.test-d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test } from 'vitest' -import { Graffle } from '../../../tests/_/schema/generated/__.js' -import { schema } from '../../../tests/_/schema/schema.js' -import { QueryOnly } from '../../../tests/_/schemaQueryOnly/generated/__.js' - -test(`works`, () => { - Graffle.create({ schema, returnMode: `graphql` }) - Graffle.create({ schema, returnMode: `data` }) - Graffle.create({ schema, returnMode: `dataAndErrors` }) - Graffle.create({ schema, returnMode: `dataSuccess` }) - - QueryOnly.create({ schema, returnMode: `graphql` }) - QueryOnly.create({ schema, returnMode: `data` }) - QueryOnly.create({ schema, returnMode: `dataAndErrors` }) - // @ts-expect-error bad returnMode - QueryOnly.create({ schema, name: `QueryOnly`, returnMode: `dataSuccess` }) -}) diff --git a/src/layers/6_client/client.returnMode.test-d.ts b/src/layers/6_client/client.returnMode.test-d.ts deleted file mode 100644 index 0df3e9d80..000000000 --- a/src/layers/6_client/client.returnMode.test-d.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable */ -import { ExecutionResult } from 'graphql' -import { type ObjMap } from 'graphql/jsutils/ObjMap.js' -import { describe } from 'node:test' -import { expectTypeOf, test } from 'vitest' -import { Graffle } from '../../../tests/_/schema/generated/__.js' -import { schema } from '../../../tests/_/schema/schema.js' -import { GraphQLExecutionResultError } from '../../lib/graphql.js' - -// dprint-ignore -describe('default is data', () => { - const graffle = Graffle.create({ schema }) - test(`document.run`, async () => { - expectTypeOf(graffle.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null }>() - }) - test('query.', async () => { - await expectTypeOf(graffle.query.__typename()).resolves.toEqualTypeOf<'Query'>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null }>() - }) - test('query.', async () => { - await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | {__typename: "ErrorOne"} | {__typename: "ErrorTwo"} | null>() - }) - test(`raw`, async () => { - expectTypeOf(graffle.raw({ document:'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() - }) -}) - -// dprint-ignore -describe('data', () => { - const graffle = Graffle.create({ schema, returnMode: 'data' }) - test(`document.run`, async () => { - expectTypeOf(graffle.document({ x: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null }>() - }) - test('query.', async () => { - await expectTypeOf(graffle.query.__typename()).resolves.toEqualTypeOf<'Query'>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null }>() - }) - test('query.',async () => { - await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | {__typename: "ErrorOne"} | {__typename: "ErrorTwo"} | null>() - }) - test('query. orThrow',async () => { - await expectTypeOf(graffle.query.resultOrThrow({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | null>() - }) - test(`raw`, async () => { - expectTypeOf(graffle.raw({ document:'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() - }) -}) - -// dprint-ignore -describe('dataSuccess', () => { - const graffle = Graffle.create({ schema, returnMode: 'dataSuccess' }) - test(`document.run`, async () => { - expectTypeOf(graffle.document({ x: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null }>() - }) - test(`document.runOrThrow`, async () => { - expectTypeOf(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqualTypeOf<{ id: string | null }>() - }) - test('query.', async () => { - await expectTypeOf(graffle.query.__typename()).resolves.toEqualTypeOf<'Query'>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null }>() - }) - describe('result field', () => { - test('document.run',async () => { - await expectTypeOf(graffle.document({x:{query:{result:{$:{case:'Object1'},__typename:true}}}}).run()).resolves.toEqualTypeOf<{result:{__typename: "Object1"} | null}>() - }) - test('query.',async () => { - await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf<{__typename: "Object1"} | null>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ result:{$:{case:'Object1'},__typename:true} })).resolves.toEqualTypeOf<{result:{__typename: "Object1"} | null}>() - }) - describe('without explicit __typename', () => { - test('document',async () => { - const result = graffle.document({x:{query:{resultNonNull:{$:{case:'Object1'}}}}}).run() - await expectTypeOf(result).resolves.toEqualTypeOf<{resultNonNull:{__typename: "Object1"}}>() - }) - test('query.',async () => { - await expectTypeOf(graffle.query.result({$:{case:'Object1'}})).resolves.toEqualTypeOf<{__typename: "Object1"} | null>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ result:{$:{case:'Object1'}} })).resolves.toEqualTypeOf<{result:{__typename: "Object1"} | null}>() - }) - }) - }) - test(`raw`, async () => { - expectTypeOf(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() - }) -}) - -// dprint-ignore -describe('dataAndErrors', () => { - const graffle = Graffle.create({ schema, returnMode: 'dataAndErrors' }) - test(`document`, async () => { - expectTypeOf(graffle.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null } | GraphQLExecutionResultError>() - }) - test(`document runOrThrow`, async () => { - expectTypeOf(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqualTypeOf<{ id: string | null }>() - }) - test('query.', async () => { - await expectTypeOf(graffle.query.__typename()).resolves.toEqualTypeOf<'Query' | GraphQLExecutionResultError>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null } | GraphQLExecutionResultError>() - }) - test('query.',async () => { - 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({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() - }) -}) - -// dprint-ignore -describe('graphql', () => { - const graffle = Graffle.create({ schema, returnMode: 'graphql' }) - test(`document.run`, async () => { - expectTypeOf(graffle.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf>>() - }) - test(`document.runOrThrow`, async () => { - expectTypeOf(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqualTypeOf>>() - }) - test('query.', async () => { - await expectTypeOf(graffle.query.__typename()).resolves.toEqualTypeOf>() - }) - test('query.OrThrow', async () => { - await expectTypeOf(graffle.query.__typenameOrThrow()).resolves.toEqualTypeOf>() - }) - test('query.$batch', async () => { - await expectTypeOf(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf>() - }) - test('query.$batchOrThrow', async () => { - await expectTypeOf(graffle.query.$batchOrThrow({ __typename: true, id: true })).resolves.toEqualTypeOf>() - }) - test('query.',async () => { - await expectTypeOf(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqualTypeOf>() - }) - test(`raw`, async () => { - expectTypeOf(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqualTypeOf() - }) -}) - -type z = { - __typename: 'ErrorOne' -} | { - __typename: 'ErrorTwo' -} - -type y = Exclude<{ id: string | null } | {} | {} | null, { id: 1 }> -type y2 = Exclude<{ id: string | null } | {} | {} | null, z> diff --git a/src/layers/6_client/client.returnMode.test.ts b/src/layers/6_client/client.returnMode.test.ts deleted file mode 100644 index b9218a482..000000000 --- a/src/layers/6_client/client.returnMode.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* eslint-disable */ -import { describe, expect, test } from 'vitest' -import { db } from '../../../tests/_/db.js' -import { Graffle } from '../../../tests/_/schema/generated/__.js' -import { schema } from '../../../tests/_/schema/schema.js' -import { __typename } from '../1_Schema/_.js' - -// dprint-ignore -describe('default (data)', () => { - const graffle = Graffle.create({ schema }) - test(`document`, async () => { - await expect(graffle.document({ main: { query: { id: true } } }).run()).resolves.toEqual({ id: db.id }) - }) - test(`document.runOrThrow`, async () => { - await expect(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({ id: db.id }) - }) - test(`document.runOrThrow error`, async () => { - await expect(graffle.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.errorAggregate) - }) - test('raw', async () => { - 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') - }) - test('query. error', async () => { - await expect(graffle.query.error()).rejects.toMatchObject(db.errorAggregate) - }) - test('query. error orThrow', async () => { - await expect(graffle.query.errorOrThrow()).rejects.toMatchObject(db.errorAggregate) - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Query', id: db.id }) - }) - test('query.$batchOrThrow error', async () => { - await expect(graffle.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.errorAggregate) - }) - test('mutation.', async () => { - await expect(graffle.mutation.__typename()).resolves.toEqual('Mutation') - }) - test('mutation.$batch', async () => { - await expect(graffle.mutation.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Mutation', id: db.id }) - }) -}) - -// dprint-ignore -describe('dataAndErrors', () => { - const graffle = Graffle.create({ schema, returnMode: 'dataAndErrors' }) - test(`document.run`, async () => { - await expect(graffle.document({ main: { query: { id: true } } }).run()).resolves.toEqual({ id: db.id }) - }) - test(`document.runOrThrow`, async () => { - await expect(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({ id: db.id }) - }) - test(`document.runOrThrow error`, async () => { - await expect(graffle.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.errorAggregate) - }) - test('raw', async () => { - 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') - }) - test('query. error', async () => { - await expect(graffle.query.error()).resolves.toMatchObject(db.errorAggregate) - }) - test('query. error orThrow', async () => { - await expect(graffle.query.errorOrThrow()).rejects.toMatchObject(db.errorAggregate) - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Query', id: db.id }) - }) - test('query.$batchOrThrow error', async () => { - await expect(graffle.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.errorAggregate) - }) - test('mutation.', async () => { - await expect(graffle.mutation.__typename()).resolves.toEqual('Mutation') - }) - test('mutation.$batch', async () => { - await expect(graffle.mutation.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Mutation', id: db.id }) - }) -}) - -// dprint-ignore -describe('dataSuccess', () => { - const graffle = Graffle.create({ schema, returnMode: 'dataSuccess' }) - test(`document.run`, async () => { - expect(graffle.document({ x: { query: { id: true } } }).run()).resolves.toEqual({ id: db.id }) - }) - test(`document.runOrThrow`, async () => { - expect(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({ id: db.id }) - }) - test('query.', async () => { - await expect(graffle.query.__typename()).resolves.toEqual('Query') - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Query', id: db.id }) - }) - describe('result field', () => { - test('document.run',async () => { - await expect(graffle.document({x:{query:{result:{$:{case:'Object1'},__typename:true}}}}).run()).resolves.toEqual({result:{__typename: "Object1"}}) - }) - test('query.', async () => { - await expect(graffle.query.result({$:{case:'Object1'},__typename:true})).resolves.toEqual({ __typename: "Object1" }) - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ result:{$:{case:'Object1'},__typename:true} })).resolves.toEqual({result:{__typename: "Object1"}}) - }) - describe('without explicit __typename', () => { - test('document', async () => { - await expect(graffle.document({x:{query:{result:{$:{case:'Object1'}}}}}).run()).resolves.toEqual({result:{__typename: "Object1"}}) - }) - test('query.', async () => { - await expect(graffle.query.result({$:{case:'Object1'}})).resolves.toEqual({__typename: "Object1"}) - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ result:{$:{case:'Object1'}} })).resolves.toEqual({result:{__typename: "Object1"}}) - }) - }) - describe('throws', async () => { - test('document', async () => { - await expect(graffle.document({x:{query:{result:{$:{case:'ErrorOne'}}}}}).run()).rejects.toEqual(db.ErrorOneError) - }) - test('query.', async () => { - await expect(graffle.query.result({$:{case:'ErrorOne'}})).rejects.toEqual(db.ErrorOneError) - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ result:{$:{case:'ErrorOne'}} })).rejects.toEqual(db.ErrorOneError) - }) - // todo result twice, using aliases, check that aggregate error is thrown - }) - }) - test(`raw`, async () => { - expect(graffle.raw({ document: 'query main {\nid\n}', operationName: 'main' })).resolves.toEqual({data:{id:db.id}}) - }) -}) - -// dprint-ignore -describe('graphql', () => { - const graffle = Graffle.create({ schema, returnMode: 'graphql' }) - test(`document.run`, async () => { - await expect(graffle.document({ main: { query: { id: true } } }).run()).resolves.toEqual({ data: { id: db.id } }) // dprint-ignore - }) - test(`document.runOrThrow`, async () => { - await expect(graffle.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({data:{ id: db.id }}) - }) - test(`document.runOrThrow error`, async () => { - await expect(graffle.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.errorAggregate) - }) - test('raw', async () => { - 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' } }) - }) - test('query. error', async () => { - await expect(graffle.query.error()).resolves.toMatchObject({ errors:db.errorAggregate['errors'] }) - }) - test('query. orThrow error', async () => { - await expect(graffle.query.errorOrThrow()).rejects.toMatchObject(db.errorAggregate) - }) - test('query.$batch', async () => { - await expect(graffle.query.$batch({ __typename: true, id: true })).resolves.toEqual({ data: { __typename: 'Query', id: db.id } }) - }) - test('query.$batchOrThrow error', async () => { - await expect(graffle.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.errorAggregate) - }) - test('mutation.', async () => { - await expect(graffle.mutation.__typename()).resolves.toEqual({ data: { __typename: 'Mutation' } }) - }) - test('mutation.$batch', async () => { - await expect(graffle.mutation.$batch({ __typename: true, id: true })).resolves.toEqual({ data: { __typename: 'Mutation', id: db.id } }) - }) -}) diff --git a/src/layers/6_client/client.test.ts b/src/layers/6_client/client.test.ts index 24c66cd7d..53d70f724 100644 --- a/src/layers/6_client/client.test.ts +++ b/src/layers/6_client/client.test.ts @@ -1,11 +1,14 @@ -import { describe, expect } from 'vitest' +import { describe, expect, expectTypeOf } from 'vitest' import { createResponse, test } from '../../../tests/_/helpers.js' +import { Graffle as Graffle2 } from '../../../tests/_/schema/generated/__.js' +import { schema } from '../../../tests/_/schema/schema.js' import { Graffle } from '../../entrypoints/alpha/main.js' import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from '../../lib/http.js' +const schemaUrl = new URL(`https://foo.io/api/graphql`) + describe(`without schemaIndex only raw is available`, () => { - const schema = new URL(`https://foo.io/api/graphql`) - const graffle = Graffle.create({ schema }) + const graffle = Graffle.create({ schema: schemaUrl }) test(`unavailable methods`, () => { // @ts-expect-error @@ -35,3 +38,25 @@ describe(`interface`, () => { }) }) }) + +describe(`output`, () => { + test(`when using envelope and transport is http, response property is available`, async ({ fetch }) => { + fetch.mockImplementationOnce(() => Promise.resolve(createResponse({ data: { id: `abc` } }))) + const graffle = Graffle2.create({ schema: schemaUrl, output: { envelope: true } }) + const result = await graffle.query.id() + expectTypeOf(result.response).toEqualTypeOf() + expect(result.response.status).toEqual(200) + // sanity check + expect(result.data).toEqual({ 'id': `abc` }) + }) + test(`when using envelope and transport is memory, response property is NOT available`, async () => { + const graffle = Graffle2.create({ schema, output: { envelope: true } }) + const result = await graffle.query.id() + // @ts-expect-error not present + expectTypeOf(result.response).toEqualTypeOf() + // @ts-expect-error not present + expect(result.response).toEqual(undefined) + // sanity check + expect(result.data).toEqual({ 'id': `abc` }) + }) +}) diff --git a/src/layers/6_client/client.ts b/src/layers/6_client/client.ts index 8a5600a7f..3844424d2 100644 --- a/src/layers/6_client/client.ts +++ b/src/layers/6_client/client.ts @@ -3,6 +3,7 @@ import type { Anyware } from '../../lib/anyware/__.js' import { Errors } from '../../lib/errors/__.js' import type { SomeExecutionResultWithoutErrors } from '../../lib/graphql.js' import { isOperationTypeName, operationTypeNameToRootTypeName, type RootTypeName } from '../../lib/graphql.js' +import type { Exact } from '../../lib/prelude.js' import { isPlainObject } from '../../lib/prelude.js' import type { URLInput } from '../0_functions/request.js' import type { BaseInput } from '../0_functions/types.js' @@ -13,22 +14,31 @@ import type { DocumentObject, GraphQLObjectSelection } from '../3_SelectionSet/e import { Core } from '../5_core/__.js' import { type HookDefEncode } from '../5_core/core.js' import type { InterfaceRaw } from '../5_core/types.js' -import type { - ApplyInputDefaults, - Config, - ReturnModeType, - ReturnModeTypeBase, - ReturnModeTypeDataSuccess, -} from './Config.js' import type { DocumentFn } from './document.js' import type { GetRootTypeMethods } from './RootTypeMethods.js' +import type { Envelope } from './Settings/Config.js' +import { + type Config, + isContextConfigTraditionalGraphQLOutput, + readConfigErrorCategoryOutputChannel, + traditionalGraphqlOutput, + traditionalGraphqlOutputThrowing, +} from './Settings/Config.js' +import { type Input, type InputPrefilled, type InputToConfig, inputToConfig } from './Settings/Input.js' export type SchemaInput = URLInput | GraphQLSchema // todo could list specific errors here // Anyware entrypoint // Extension -type GraffleExecutionResult = ExecutionResult | Errors.ContextualError +type GraffleExecutionResult = + | (ExecutionResult & { + /** + * If transport was HTTP, then the raw response is available here. + */ + response?: Response + }) + | Errors.ContextualError export type SelectionSetOrIndicator = 0 | 1 | boolean | object @@ -56,8 +66,8 @@ type RawParameters = ] // todo no config needed? -export type ClientRaw<_$Config extends Config> = { - raw(input: RawInput): Promise +export type ClientRaw<$Config extends Config> = { + raw(input: RawInput): Promise> // todo test this overload raw( document: RawInput['document'], @@ -95,30 +105,21 @@ export type ClientTyped<$Index extends Schema.Index, $Config extends Config> = } & GetRootTypeMethods<$Config, $Index> -export type InputRaw = { - schema: SchemaInput - // todo condition on if schema is NOT GraphQLSchema - headers?: HeadersInit -} - -export type InputPrefilled<$Schema extends GlobalRegistry.SchemaList> = $Schema extends any ? { - returnMode?: - | ReturnModeTypeBase - | (GlobalRegistry.HasSchemaErrors<$Schema> extends true ? ReturnModeTypeDataSuccess : never) - } & InputRaw - : never - +// dprint-ignore export type CreatePrefilled = <$Name extends GlobalRegistry.SchemaNames>(name: $Name, schemaIndex: Schema.Index) => < // eslint-disable-next-line // @ts-ignore passes after generation $Input extends InputPrefilled, >( - input: $Input, -) => Client< + // eslint-disable-next-line + // @ts-ignore passes after generation + input: Exact<$Input, InputPrefilled>, +) => +Client< // eslint-disable-next-line // @ts-ignore passes after generation GlobalRegistry.GetSchemaIndexOrDefault<$Name>, - ApplyInputDefaults<{ returnMode: $Input['returnMode'] }> + InputToConfig<$Input> > export const createPrefilled: CreatePrefilled = (name, schemaIndex) => { @@ -127,45 +128,22 @@ export const createPrefilled: CreatePrefilled = (name, schemaIndex) => { return (input) => create({ ...input, name, schemaIndex }) as any } -export type Input<$Schema extends GlobalRegistry.SchemaList> = { - /** - * Used internally. - * - * When custom scalars are being used, this runtime schema is used to - * encode/decode them before/after your application sends/receives them. - * - * When using root type field methods, this runtime schema is used to assist how arguments on scalars versus objects - * are constructed into the sent GraphQL document. - */ - readonly schemaIndex?: Schema.Index | null - /** - * The schema to use. - * - * TODO why don't we infer this from the runtime schemaIndex? - * - * @defaultValue 'default' - */ - name?: $Schema['index']['name'] - // todo way to hide Relay input pattern of nested input - // elideInputKey: true, -} & InputPrefilled<$Schema> - // dprint-ignore type Create = < $Input extends Input, >( input: $Input, ) => - Client< - // eslint-disable-next-line - // @ts-ignore passes after generation - $Input['schemaIndex'] extends Schema.Index - // v-- TypeScript does not understand this type satisfies the Index constraint. - // v It does after generation. - ? GlobalRegistry.GetSchemaIndexOrDefault<$Input['name']> - : null, - ApplyInputDefaults<{ returnMode: $Input['returnMode'] }> - > +Client< + // eslint-disable-next-line + // @ts-ignore passes after generation + $Input['schemaIndex'] extends Schema.Index + // v-- TypeScript does not understand this type satisfies the Index constraint. + // v It does after generation. + ? GlobalRegistry.GetSchemaIndexOrDefault<$Input['name']> + : null, + InputToConfig<$Input> +> export const create: Create = ( input_, @@ -190,7 +168,7 @@ export const createInternal = ( * However our implementation here needs to be generic and support all return modes * so we force cast it as such. */ - const returnMode = input.returnMode ?? `data` as ReturnModeType + // const returnMode = input.returnMode ?? `data` as ReturnModeType const executeRootType = async ( context: TypedContext, @@ -232,7 +210,9 @@ export const createInternal = ( const isSelectedTypeScalarOrTypeName = selectedNamedType.kind === `Scalar` || selectedNamedType.kind === `typename` // todo fix type here, its valid const isFieldHasArgs = Boolean(context.schemaIndex.Root[rootTypeName]?.fields[rootTypeFieldName]?.args) // We should only need to add __typename for result type fields, but the return handler doesn't yet know how to look beyond a plain object type so we have to add all those cases here. - const needsTypenameAdded = context.config.returnMode === `dataSuccess` + // todo we could look at the root type fields that have result types and compare to the incoming query for match? + const isHasSchemaErrors = Object.values(context.schemaIndex.error.objects).length > 0 + const needsTypenameAdded = isHasSchemaErrors && context.config.output.errors.schema !== false && (selectedNamedType.kind === `Object` || selectedNamedType.kind === `Interface` || selectedNamedType.kind === `Union`) const rootTypeFieldSelectionSet = isSelectedTypeScalarOrTypeName @@ -245,11 +225,11 @@ export const createInternal = ( [rootTypeFieldName]: rootTypeFieldSelectionSet, } as GraphQLObjectSelection) if (result instanceof Error) return result - return context.config.returnMode === `data` || context.config.returnMode === `dataAndErrors` - || context.config.returnMode === `dataSuccess` + + return context.config.output.envelope.enabled + ? result // @ts-expect-error - ? result[rootTypeFieldName] - : result + : result[rootTypeFieldName] } const createRootTypeMethods = (context: TypedContext, rootTypeName: RootTypeName) => { @@ -259,7 +239,7 @@ export const createInternal = ( // todo We need to document that in order for this to 100% work none of the user's root type fields can end with "OrThrow". const isOrThrow = key.endsWith(`OrThrow`) - const contextWithReturnModeSet = isOrThrow ? applyOrThrowToContext(context) : context + const contextWithReturnModeSet = isOrThrow ? contextConfigSetOrThrow(context) : context if (key.startsWith(`$batch`)) { return async (selectionSetOrIndicator: SelectionSetOrIndicator) => @@ -276,9 +256,7 @@ export const createInternal = ( const context: Context = { retry: state.retry, extensions: state.extensions, - config: { - returnMode, - }, + config: inputToConfig(input), } const run = async (context: Context, initialInput: HookDefEncode['input']) => { @@ -287,7 +265,7 @@ export const createInternal = ( retryingExtension: context.retry, extensions: context.extensions.filter(_ => _.anyware !== undefined).map(_ => _.anyware!), }) as GraffleExecutionResult - return handleReturn(context, result) + return handleOutput(context, result) } const runRaw = async (context: Context, rawInput: RawInput) => { @@ -318,13 +296,16 @@ export const createInternal = ( const client: Client = { raw: async (...args: RawParameters) => { const input = resolveRawParameters(args) - const contextWithReturnModeSet = updateContextConfig(context, { returnMode: `graphql` }) - return await runRaw(contextWithReturnModeSet, input) + const contextWithOutputSet = updateContextConfig(context, { ...context.config, output: traditionalGraphqlOutput }) + return await runRaw(contextWithOutputSet, input) }, rawOrThrow: async (...args: RawParameters) => { const input = resolveRawParameters(args) - const contextWithReturnModeSet = updateContextConfig(context, { returnMode: `graphqlSuccess` }) - return await runRaw(contextWithReturnModeSet, input) + const contextWithOutputSet = updateContextConfig(context, { + ...context.config, + output: traditionalGraphqlOutputThrowing, + }) + return await runRaw(contextWithOutputSet, input) }, // todo $use use: (extensionOrAnyware: Extension | Anyware.Extension2) => { @@ -382,7 +363,7 @@ export const createInternal = ( runOrThrow: async (maybeOperationName: string) => { const { selection, rootTypeName } = processInput(maybeOperationName) return await executeRootType( - applyOrThrowToContext(typedContext), + contextConfigSetOrThrow(typedContext), rootTypeName, selection, ) @@ -399,80 +380,140 @@ export const createInternal = ( return client } -const handleReturn = ( +const handleOutput = ( context: Context, result: GraffleExecutionResult, ) => { - switch (context.config.returnMode) { - case `graphqlSuccess`: - case `dataAndErrors`: - case `dataSuccess`: - case `data`: { - if (result instanceof Error || (result.errors && result.errors.length > 0)) { - const error = result instanceof Error ? result : (new Errors.ContextualAggregateError( - `One or more errors in the execution result.`, - {}, - result.errors!, - )) - if ( - context.config.returnMode === `data` || context.config.returnMode === `dataSuccess` - || context.config.returnMode === `graphqlSuccess` - ) throw error - return error - } + if (isContextConfigTraditionalGraphQLOutput(context.config)) return result - if (isTypedContext(context)) { - if (context.config.returnMode === `dataSuccess`) { - if (!isPlainObject(result.data)) throw new Error(`Expected data to be an object.`) - const schemaErrors = Object.entries(result.data).map(([rootFieldName, rootFieldValue]) => { - // todo this check would be nice but it doesn't account for aliases right now. To achieve this we would - // need to have the selection set available to use and then do a costly analysis for all fields that were aliases. - // So costly that we would probably instead want to create an index of them on the initial encoding step and - // then make available down stream. Also, note, here, the hardcoding of Query, needs to be any root type. - // const isResultField = Boolean(schemaIndex.error.rootResultFields.Query[rootFieldName]) - // if (!isResultField) return null - // if (!isPlainObject(rootFieldValue)) return new Error(`Expected result field to be an object.`) - if (!isPlainObject(rootFieldValue)) return null - const __typename = rootFieldValue[`__typename`] - if (typeof __typename !== `string`) throw new Error(`Expected __typename to be selected and a string.`) - const isErrorObject = Boolean( - context.schemaIndex.error.objectsTypename[__typename], - ) - if (!isErrorObject) return null - // todo extract message - return new Error(`Failure on field ${rootFieldName}: ${__typename}`) - }).filter((_): _ is Error => _ !== null) - - if (schemaErrors.length === 1) throw schemaErrors[0]! - if (schemaErrors.length > 0) { - const error = new Errors.ContextualAggregateError( - `Two or more schema errors in the execution result.`, - {}, - schemaErrors, - ) - throw error + const c = context.config.output + + const isEnvelope = c.envelope.enabled + + const isThrowOther = readConfigErrorCategoryOutputChannel(context.config, `other`) === `throw` + && (!c.envelope.enabled || !c.envelope.errors.other) + + const isReturnOther = readConfigErrorCategoryOutputChannel(context.config, `other`) === `return` + && (!c.envelope.enabled || !c.envelope.errors.other) + + const isThrowExecution = readConfigErrorCategoryOutputChannel(context.config, `execution`) === `throw` + && (!c.envelope.enabled || !c.envelope.errors.execution) + + const isReturnExecution = readConfigErrorCategoryOutputChannel(context.config, `execution`) === `return` + && (!c.envelope.enabled || !c.envelope.errors.execution) + + const isThrowSchema = readConfigErrorCategoryOutputChannel(context.config, `schema`) === `throw` + + const isReturnSchema = readConfigErrorCategoryOutputChannel(context.config, `schema`) === `return` + + if (result instanceof Error) { + if (isThrowOther) throw result + if (isReturnOther) return result + // todo not a graphql execution error class instance + return isEnvelope ? { errors: [result] } : result + } + + if (result.errors && result.errors.length > 0) { + const error = new Errors.ContextualAggregateError( + `One or more errors in the execution result.`, + {}, + result.errors, + ) + if (isThrowExecution) throw error + if (isReturnExecution) return error + return isEnvelope ? result : error + } + + { + if (isTypedContext(context)) { + if (c.errors.schema !== false) { + if (!isPlainObject(result.data)) throw new Error(`Expected data to be an object.`) + const schemaErrors = Object.entries(result.data).map(([rootFieldName, rootFieldValue]) => { + // todo this check would be nice but it doesn't account for aliases right now. To achieve this we would + // need to have the selection set available to use and then do a costly analysis for all fields that were aliases. + // So costly that we would probably instead want to create an index of them on the initial encoding step and + // then make available down stream. Also, note, here, the hardcoding of Query, needs to be any root type. + // const isResultField = Boolean(schemaIndex.error.rootResultFields.Query[rootFieldName]) + // if (!isResultField) return null + // if (!isPlainObject(rootFieldValue)) return new Error(`Expected result field to be an object.`) + if (!isPlainObject(rootFieldValue)) return null + const __typename = rootFieldValue[`__typename`] + if (typeof __typename !== `string`) throw new Error(`Expected __typename to be selected and a string.`) + const isErrorObject = Boolean( + context.schemaIndex.error.objectsTypename[__typename], + ) + if (!isErrorObject) return null + // todo extract message + // todo allow mapping error instances to schema errors + return new Error(`Failure on field ${rootFieldName}: ${__typename}`) + }).filter((_): _ is Error => _ !== null) + + const error = (schemaErrors.length === 1) + ? schemaErrors[0]! + : schemaErrors.length > 0 + ? new Errors.ContextualAggregateError( + `Two or more schema errors in the execution result.`, + {}, + schemaErrors, + ) + : null + if (error) { + if (isThrowSchema) throw error + if (isReturnSchema) { + return isEnvelope ? { ...result, errors: [...result.errors ?? [], error] } : error } } } - - if (context.config.returnMode === `graphqlSuccess`) { - return result - } - - return result.data } - default: { + + if (isEnvelope) { return result } + + return result.data } } -const applyOrThrowToContext = <$Context extends Context>(context: $Context): $Context => { - if (context.config.returnMode === `dataSuccess` || context.config.returnMode === `graphqlSuccess`) { - return context +const contextConfigSetOrThrow = <$Context extends Context>(context: $Context): $Context => { + if (isContextConfigOrThrowSemantics(context)) return context + + return updateContextConfig(context, { + ...context.config, + output: { + ...context.config.output, + errors: { + execution: `throw`, + other: `throw`, + schema: `throw`, + }, + envelope: { + ...context.config.output.envelope, + errors: { + execution: false, + other: false, + schema: false, + }, + }, + }, + }) +} + +const isContextConfigOrThrowSemantics = ({ config }: Context): boolean => { + const isAllCategoriesThrowOrDisabled = readConfigErrorCategoryOutputChannel(config, `execution`) === `throw` + && readConfigErrorCategoryOutputChannel(config, `other`) === `throw` + && (readConfigErrorCategoryOutputChannel(config, `schema`) === `throw` + || readConfigErrorCategoryOutputChannel(config, `schema`) === `throw`) // todo: or false and not using schema errors + + if (!isAllCategoriesThrowOrDisabled) return false + + if ( + config.output.envelope.enabled + && Object.values(config.output.envelope.errors.execution).filter(_ => _ === true).length > 0 + ) { + return false } - const newMode = context.config.returnMode === `graphql` ? `graphqlSuccess` : `dataSuccess` - return updateContextConfig(context, { returnMode: newMode }) + + return true } const updateContextConfig = <$Context extends Context>(context: $Context, config: Config): $Context => { diff --git a/src/layers/6_client/client.document.test-d.ts b/src/layers/6_client/document.test-d.ts similarity index 100% rename from src/layers/6_client/client.document.test-d.ts rename to src/layers/6_client/document.test-d.ts diff --git a/src/layers/6_client/client.document.test.ts b/src/layers/6_client/document.test.ts similarity index 100% rename from src/layers/6_client/client.document.test.ts rename to src/layers/6_client/document.test.ts diff --git a/src/layers/6_client/document.ts b/src/layers/6_client/document.ts index 33d278296..7a976d984 100644 --- a/src/layers/6_client/document.ts +++ b/src/layers/6_client/document.ts @@ -6,7 +6,12 @@ import type { Schema } from '../1_Schema/__.js' import { SelectionSet } from '../3_SelectionSet/__.js' import type { Context, DocumentObject } from '../3_SelectionSet/encode.js' import type { ResultSet } from '../4_ResultSet/__.js' -import type { AugmentRootTypeSelectionWithTypename, Config, OrThrowifyConfig, ReturnModeRootType } from './Config.js' +import type { + AugmentRootTypeSelectionWithTypename, + Config, + OrThrowifyConfig, + ResolveOutputReturnRootType, +} from './Settings/Config.js' // dprint-ignore export type DocumentFn<$Config extends Config, $Index extends Schema.Index> = @@ -16,13 +21,18 @@ export type DocumentFn<$Config extends Config, $Index extends Schema.Index> = $Name extends keyof $Document & string, $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([] | [name: $Name | undefined])), >(...params: $Params) => Promise< - ReturnModeRootType<$Config, $Index, ResultSet.Root, $Index, GetRootType<$Document[$Name]>>> + ResolveOutputReturnRootType<$Config, $Index, ResultSet.Root, $Index, GetRootType<$Document[$Name]>>> > runOrThrow: < $Name extends keyof $Document & string, $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([] | [name: $Name | undefined])), >(...params: $Params) => Promise< - ReturnModeRootType, $Index, ResultSet.Root, $Index, $Document[$Name]>, $Index, GetRootType<$Document[$Name]>>> + ResolveOutputReturnRootType< + // @ts-expect-error fixme + OrThrowifyConfig<$Config>, + $Index, + // @ts-expect-error fixme + ResultSet.Root, $Index, $Document[$Name]>, $Index, GetRootType<$Document[$Name]>>> > } diff --git a/src/layers/6_client/client.rootTypeMethods.test-d.ts b/src/layers/6_client/rootTypeMethods.test-d.ts similarity index 100% rename from src/layers/6_client/client.rootTypeMethods.test-d.ts rename to src/layers/6_client/rootTypeMethods.test-d.ts diff --git a/src/layers/6_client/client.rootTypeMethods.test.ts b/src/layers/6_client/rootTypeMethods.test.ts similarity index 100% rename from src/layers/6_client/client.rootTypeMethods.test.ts rename to src/layers/6_client/rootTypeMethods.test.ts diff --git a/src/layers/7_extensions/Upload/Upload.test.ts b/src/layers/7_extensions/Upload/Upload.test.ts index cbae3c1cf..c01717616 100644 --- a/src/layers/7_extensions/Upload/Upload.test.ts +++ b/src/layers/7_extensions/Upload/Upload.test.ts @@ -1,6 +1,7 @@ // todo in order to test jsdom, we need to boot the server in a separate process // @vitest-environment node +import { omit } from 'es-toolkit' import getPort from 'get-port' import type { Server } from 'node:http' import { createServer } from 'node:http' @@ -11,10 +12,11 @@ import { Upload } from './Upload.js' import { createYoga } from 'graphql-yoga' import type { Client } from '../../6_client/client.js' +import type { OutputConfigDefault } from '../../6_client/Settings/Config.js' let server: Server let port: number -let graffle: Client +let graffle: Client beforeAll(async () => { const yoga = createYoga({ schema }) @@ -54,7 +56,7 @@ test(`upload`, async () => { blob: new Blob([`Hello World`], { type: `text/plain` }) as any, // eslint-disable-line }, }) - expect(result).toMatchInlineSnapshot(` + expect(omit(result, [`response`])).toMatchInlineSnapshot(` { "data": { "readTextFile": "Hello World", diff --git a/src/lib/prelude.test-d.ts b/src/lib/prelude.test-d.ts new file mode 100644 index 000000000..fee65e43a --- /dev/null +++ b/src/lib/prelude.test-d.ts @@ -0,0 +1,12 @@ +import { expectTypeOf, test } from 'vitest' +import type { ConfigManager } from './prelude.js' + +test(`ConfigManager`, () => { + expectTypeOf>().toEqualTypeOf<{ a: { b: 2 }; a2: 2 }>() + expectTypeOf>().toEqualTypeOf<{ a: { b: 3 } }>() + expectTypeOf>().toEqualTypeOf<{ a: { b: 3 } }>() + // never + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<{ a: { b: never } }>() +}) diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 8bb0dbbbe..21c9a389c 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -1,3 +1,4 @@ +import type { Simplify } from 'type-fest' import type { ConditionalSimplifyDeep } from 'type-fest/source/conditional-simplify.js' /* eslint-disable */ @@ -217,10 +218,6 @@ export const mapValues = < ) as Record> } -export type SetProperty<$Obj extends object, $Prop extends keyof $Obj, $Type extends $Obj[$Prop]> = - & Omit<$Obj, $Prop> - & { [_ in $Prop]: $Type } - export const lowerCaseFirstLetter = (s: string) => { return s.charAt(0).toLowerCase() + s.slice(1) } @@ -340,3 +337,47 @@ export const partitionErrors = (array: T[]): [Exclude[], Include = OrDefault, $Default> + + export type OrDefault<$Value, $Default> = $Value extends undefined ? $Default : $Value + + // dprint-ignore + export type Read<$Value, $Path extends [...string[]]> = + $Value extends undefined ? undefined + : $Path extends [infer P1 extends string, ...infer PN extends string[]] ? + $Value extends object ? P1 extends keyof $Value ? Read<$Value[P1], PN> : undefined + : undefined + : $Value + + export type SetProperty<$Obj extends object, $Prop extends keyof $Obj, $Type extends $Obj[$Prop]> = + & Omit<$Obj, $Prop> + & { [_ in $Prop]: $Type } + + // dprint-ignore + export type Set<$Obj extends object, $Path extends Path, $Value> = + Simplify< + $Path extends [] + ? $Value extends object + ? $Obj & $Value + : never + : Set_<$Obj, $Path, $Value> + > + + // dprint-ignore + export type Set_<$ObjOrValue, $Path extends Path, $Value> = + Simplify< + $Path extends [infer $P1 extends string, ...infer $PN extends string[]] ? + $P1 extends keyof $ObjOrValue + ? Omit<$ObjOrValue, $P1> & { [_ in $P1]: Set_<$ObjOrValue[$P1], $PN, $Value> } + // If we use a nice error display here (like the following comment) it will mess with the result type in variable cases. + // `Error: Cannot set value at path in object. Path property "${$P1}" does not exist in object.` + : never + : $Value + > +} + +// type AsBoolean = T extends boolean ? T : never diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts index 000e26199..c6cc5f961 100644 --- a/tests/_/helpers.ts +++ b/tests/_/helpers.ts @@ -23,11 +23,8 @@ export const test = testBase.extend({ await use(fetchMock) globalThis.fetch = fetch }, - graffle: async ({ fetch }, use) => { + graffle: async ({ fetch: _ }, use) => { const graffle = Graffle.create({ schema: new URL(`https://foo.io/api/graphql`) }) - .use(async ({ exchange }) => { - return exchange({ using: { fetch: fetch as typeof globalThis.fetch } }) - }) await use(graffle) }, })