From c0620580b8070499fb670ca3abacb7ad5e55aa04 Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:32:46 +0200 Subject: [PATCH] feat(2639): implement entity resolver (#2693) Co-authored-by: Tushar Mathur Co-authored-by: Amit Singh --- .../apollo_federation_subgraph_post.graphql | 21 + .../apollo_federation_subgraph_user.graphql | 18 + examples/federation/README.md | 28 + examples/federation/gateway.js | 20 + examples/federation/package-lock.json | 3117 +++++++++++++++++ examples/federation/package.json | 18 + examples/federation/rover.sh | 18 + generated/.tailcallrc.graphql | 12 +- generated/.tailcallrc.schema.json | 98 + src/core/app_context.rs | 11 +- src/core/blueprint/definitions.rs | 1 + .../blueprint/operators/apollo_federation.rs | 163 + src/core/blueprint/operators/call.rs | 15 +- src/core/blueprint/operators/expr.rs | 8 +- src/core/blueprint/operators/graphql.rs | 2 +- src/core/blueprint/operators/http.rs | 12 +- src/core/blueprint/operators/js.rs | 8 +- src/core/blueprint/operators/mod.rs | 2 + ...lueprint__index__test__from_blueprint.snap | 8 + src/core/config/config.rs | 314 +- src/core/config/directives/call.rs | 47 + src/core/config/directives/expr.rs | 23 + src/core/config/directives/federation.rs | 21 + src/core/config/directives/graphql.rs | 53 + src/core/config/directives/grpc.rs | 54 + src/core/config/directives/http.rs | 93 + src/core/config/directives/js.rs | 18 + src/core/config/directives/mod.rs | 15 + src/core/config/from_document.rs | 18 +- src/core/config/into_document.rs | 51 +- src/core/config/mod.rs | 4 + src/core/config/resolver.rs | 56 + src/core/config/transformer/mod.rs | 2 + src/core/config/transformer/required.rs | 1 + ...graph__tests__extractor__extract_call.snap | 11 + ...graph__tests__extractor__extract_expr.snap | 11 + ...ph__tests__extractor__extract_graphql.snap | 11 + ...graph__tests__extractor__extract_grpc.snap | 11 + ...graph__tests__extractor__extract_http.snap | 11 + ..._tests__extractor__non_value_template.snap | 28 + ...er__subgraph__tests__keys__keys_merge.snap | 32 + ...rmer__subgraph__tests__keys__keys_set.snap | 32 + src/core/config/transformer/subgraph.rs | 493 +++ src/core/helpers/body.rs | 1 + src/core/ir/error.rs | 5 + src/core/ir/eval.rs | 58 +- src/core/ir/model.rs | 17 +- src/core/mustache/parse.rs | 1 - src/core/valid/valid.rs | 9 + tailcall-macros/src/lib.rs | 6 +- tailcall-macros/src/merge_right.rs | 69 +- tailcall-macros/src/resolver.rs | 80 +- ...apollo-federation-entities-batch.md_0.snap | 26 + ...apollo-federation-entities-batch.md_1.snap | 17 + ...o-federation-entities-batch.md_client.snap | 73 + ...o-federation-entities-batch.md_merged.snap | 43 + ...deration-entities-validation.md_error.snap | 23 + .../apollo-federation-entities.md_0.snap | 36 + .../apollo-federation-entities.md_1.snap | 17 + .../apollo-federation-entities.md_client.snap | 73 + .../apollo-federation-entities.md_merged.snap | 40 + .../apollo-federation-entities-batch.md | 116 + .../apollo-federation-entities-validation.md | 78 + tests/execution/apollo-federation-entities.md | 77 + 64 files changed, 5474 insertions(+), 381 deletions(-) create mode 100644 examples/apollo_federation_subgraph_post.graphql create mode 100644 examples/apollo_federation_subgraph_user.graphql create mode 100644 examples/federation/README.md create mode 100644 examples/federation/gateway.js create mode 100644 examples/federation/package-lock.json create mode 100644 examples/federation/package.json create mode 100755 examples/federation/rover.sh create mode 100644 src/core/blueprint/operators/apollo_federation.rs create mode 100644 src/core/config/directives/call.rs create mode 100644 src/core/config/directives/expr.rs create mode 100644 src/core/config/directives/federation.rs create mode 100644 src/core/config/directives/graphql.rs create mode 100644 src/core/config/directives/grpc.rs create mode 100644 src/core/config/directives/http.rs create mode 100644 src/core/config/directives/js.rs create mode 100644 src/core/config/directives/mod.rs create mode 100644 src/core/config/resolver.rs create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_call.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_expr.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_graphql.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_grpc.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_http.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__non_value_template.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_merge.snap create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_set.snap create mode 100644 src/core/config/transformer/subgraph.rs create mode 100644 tests/core/snapshots/apollo-federation-entities-batch.md_0.snap create mode 100644 tests/core/snapshots/apollo-federation-entities-batch.md_1.snap create mode 100644 tests/core/snapshots/apollo-federation-entities-batch.md_client.snap create mode 100644 tests/core/snapshots/apollo-federation-entities-batch.md_merged.snap create mode 100644 tests/core/snapshots/apollo-federation-entities-validation.md_error.snap create mode 100644 tests/core/snapshots/apollo-federation-entities.md_0.snap create mode 100644 tests/core/snapshots/apollo-federation-entities.md_1.snap create mode 100644 tests/core/snapshots/apollo-federation-entities.md_client.snap create mode 100644 tests/core/snapshots/apollo-federation-entities.md_merged.snap create mode 100644 tests/execution/apollo-federation-entities-batch.md create mode 100644 tests/execution/apollo-federation-entities-validation.md create mode 100644 tests/execution/apollo-federation-entities.md diff --git a/examples/apollo_federation_subgraph_post.graphql b/examples/apollo_federation_subgraph_post.graphql new file mode 100644 index 0000000000..0546fd7139 --- /dev/null +++ b/examples/apollo_federation_subgraph_post.graphql @@ -0,0 +1,21 @@ +schema + @server(port: 8001) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") +} + +type User @http(path: "/users", query: [{key: "id", value: "{{.value.id}}"}], batchKey: ["id"]) { + id: Int! +} + +type Post @http(path: "/posts", query: [{key: "id", value: "{{.value.id}}"}], batchKey: ["id"]) { + id: Int! + userId: Int! + title: String! + body: String! + user: User @expr(body: {id: "{{.value.userId}}"}) +} diff --git a/examples/apollo_federation_subgraph_user.graphql b/examples/apollo_federation_subgraph_user.graphql new file mode 100644 index 0000000000..f904e58781 --- /dev/null +++ b/examples/apollo_federation_subgraph_user.graphql @@ -0,0 +1,18 @@ +schema + @server(port: 8002) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) { + query: Query +} + +type Query { + user(id: Int!): User @http(path: "/users/{{.args.id}}") +} + +type User @http(path: "/users", query: [{key: "id", value: "{{.value.id}}"}], batchKey: ["id"]) { + id: Int! + name: String + username: String + email: String + phone: String + website: String +} diff --git a/examples/federation/README.md b/examples/federation/README.md new file mode 100644 index 0000000000..1d8d49842f --- /dev/null +++ b/examples/federation/README.md @@ -0,0 +1,28 @@ +# Apollo Federation example + +1. Start tailcall subgraph examples: + +- `cargo run -- start examples/apollo_federation_subgraph_post.graphql` +- `cargo run -- start examples/apollo_federation_subgraph_user.graphql` + +2. Run Apollo router by one of the following methods: + +- run `@apollo/gateway` with `npm start` (with `npm install` for the first time) from "examples/federation" folder +- start apollo router with `rover.sh` script (install [apollo rover](https://www.apollographql.com/docs/rover) first) + +3. Navigate to `http://localhost:4000` and execute supergraph queries, see [examples](#query-examples) + +# Query examples + +```graphql +{ + posts { + id + title + user { + id + name + } + } +} +``` diff --git a/examples/federation/gateway.js b/examples/federation/gateway.js new file mode 100644 index 0000000000..dcb0b89760 --- /dev/null +++ b/examples/federation/gateway.js @@ -0,0 +1,20 @@ +import {ApolloServer} from "@apollo/server" +import {startStandaloneServer} from "@apollo/server/standalone" +import {ApolloGateway, IntrospectAndCompose} from "@apollo/gateway" + +const gateway = new ApolloGateway({ + supergraphSdl: new IntrospectAndCompose({ + subgraphs: [ + {name: "post", url: "http://localhost:8001/graphql"}, + {name: "user", url: "http://localhost:8002/graphql"}, + ], + }), +}) + +const server = new ApolloServer({ + gateway, + introspection: true, +}) + +const {url} = await startStandaloneServer(server) +console.log(`🚀 Server ready at ${url}`) diff --git a/examples/federation/package-lock.json b/examples/federation/package-lock.json new file mode 100644 index 0000000000..c225c43e3f --- /dev/null +++ b/examples/federation/package-lock.json @@ -0,0 +1,3117 @@ +{ + "name": "federation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "federation", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@apollo/gateway": "^2.8.4", + "@apollo/server": "^4.11.0", + "graphql": "^16.9.0" + } + }, + "node_modules/@apollo/cache-control-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz", + "integrity": "sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==", + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/composition": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@apollo/composition/-/composition-2.8.4.tgz", + "integrity": "sha512-x8USTfHAtauW2BrXZo10gFV9yoF3TIfKUu+s5tJVm/vX/zgw3OG54TC34v5c/5pMsStcZu/aYFnMkeZ54Ay7tQ==", + "dependencies": { + "@apollo/federation-internals": "2.8.4", + "@apollo/query-graphs": "2.8.4" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" + } + }, + "node_modules/@apollo/federation-internals": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.8.4.tgz", + "integrity": "sha512-m/vFu5btNfmvxZfe8B1m8jjCN/NxCYctxjdhXgQD4WGbDwtUk59+i7NuVMtX5IfmFMKycwqnbihkv5w2E00XDA==", + "dependencies": { + "@types/uuid": "^9.0.0", + "chalk": "^4.1.0", + "js-levenshtein": "^1.1.6", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" + } + }, + "node_modules/@apollo/gateway": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@apollo/gateway/-/gateway-2.8.4.tgz", + "integrity": "sha512-KxYRvRfSLDMqyOlUu9ZuOaqZLvKwGHR3F6XSw8JmHvMUjcRzVeAY6U9821ruM9g+SdYYdm0q2jYJf4/iYPAcow==", + "dependencies": { + "@apollo/composition": "2.8.4", + "@apollo/federation-internals": "2.8.4", + "@apollo/query-planner": "2.8.4", + "@apollo/server-gateway-interface": "^1.1.0", + "@apollo/usage-reporting-protobuf": "^4.1.0", + "@apollo/utils.createhash": "^2.0.0", + "@apollo/utils.fetcher": "^2.0.0", + "@apollo/utils.isnodelike": "^2.0.0", + "@apollo/utils.keyvaluecache": "^2.1.0", + "@apollo/utils.logger": "^2.0.0", + "@josephg/resolvable": "^1.0.1", + "@opentelemetry/api": "^1.0.1", + "@types/node-fetch": "^2.6.2", + "async-retry": "^1.3.3", + "loglevel": "^1.6.1", + "make-fetch-happen": "^11.0.0", + "node-abort-controller": "^3.0.1", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" + } + }, + "node_modules/@apollo/protobufjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz", + "integrity": "sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "long": "^4.0.0" + }, + "bin": { + "apollo-pbjs": "bin/pbjs", + "apollo-pbts": "bin/pbts" + } + }, + "node_modules/@apollo/query-graphs": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@apollo/query-graphs/-/query-graphs-2.8.4.tgz", + "integrity": "sha512-X2Y79efZh1RQ8aNi9iI+3ePWvyBm+oNW83P1xyxy3qBGFOBFne+YnoADK9tHM1FXLJ34cK03xxTdxY76V2Tteg==", + "dependencies": { + "@apollo/federation-internals": "2.8.4", + "deep-equal": "^2.0.5", + "ts-graphviz": "^1.5.4", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" + } + }, + "node_modules/@apollo/query-planner": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@apollo/query-planner/-/query-planner-2.8.4.tgz", + "integrity": "sha512-HNIyYeaIvj9/5Qem7p7w5we3SEaNJeTO77tpP7jkaaYxd+FOHtlwfMM0/5X814kl2eiaMposz78Kv+JF2582Gg==", + "dependencies": { + "@apollo/federation-internals": "2.8.4", + "@apollo/query-graphs": "2.8.4", + "@apollo/utils.keyvaluecache": "^2.1.0", + "chalk": "^4.1.0", + "deep-equal": "^2.0.5", + "pretty-format": "^29.0.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" + } + }, + "node_modules/@apollo/server": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.11.0.tgz", + "integrity": "sha512-SWDvbbs0wl2zYhKG6aGLxwTJ72xpqp0awb2lotNpfezd9VcAvzaUizzKQqocephin2uMoaA8MguoyBmgtPzNWw==", + "dependencies": { + "@apollo/cache-control-types": "^1.0.3", + "@apollo/server-gateway-interface": "^1.1.1", + "@apollo/usage-reporting-protobuf": "^4.1.1", + "@apollo/utils.createhash": "^2.0.0", + "@apollo/utils.fetcher": "^2.0.0", + "@apollo/utils.isnodelike": "^2.0.0", + "@apollo/utils.keyvaluecache": "^2.1.0", + "@apollo/utils.logger": "^2.0.0", + "@apollo/utils.usagereporting": "^2.1.0", + "@apollo/utils.withrequired": "^2.0.0", + "@graphql-tools/schema": "^9.0.0", + "@types/express": "^4.17.13", + "@types/express-serve-static-core": "^4.17.30", + "@types/node-fetch": "^2.6.1", + "async-retry": "^1.2.1", + "cors": "^2.8.5", + "express": "^4.17.1", + "loglevel": "^1.6.8", + "lru-cache": "^7.10.1", + "negotiator": "^0.6.3", + "node-abort-controller": "^3.1.1", + "node-fetch": "^2.6.7", + "uuid": "^9.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=14.16.0" + }, + "peerDependencies": { + "graphql": "^16.6.0" + } + }, + "node_modules/@apollo/server-gateway-interface": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@apollo/server-gateway-interface/-/server-gateway-interface-1.1.1.tgz", + "integrity": "sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==", + "dependencies": { + "@apollo/usage-reporting-protobuf": "^4.1.1", + "@apollo/utils.fetcher": "^2.0.0", + "@apollo/utils.keyvaluecache": "^2.1.0", + "@apollo/utils.logger": "^2.0.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/usage-reporting-protobuf": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz", + "integrity": "sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==", + "dependencies": { + "@apollo/protobufjs": "1.2.7" + } + }, + "node_modules/@apollo/utils.createhash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.createhash/-/utils.createhash-2.0.1.tgz", + "integrity": "sha512-fQO4/ZOP8LcXWvMNhKiee+2KuKyqIcfHrICA+M4lj/h/Lh1H10ICcUtk6N/chnEo5HXu0yejg64wshdaiFitJg==", + "dependencies": { + "@apollo/utils.isnodelike": "^2.0.1", + "sha.js": "^2.4.11" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.dropunuseddefinitions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-2.0.1.tgz", + "integrity": "sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.fetcher": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-2.0.1.tgz", + "integrity": "sha512-jvvon885hEyWXd4H6zpWeN3tl88QcWnHp5gWF5OPF34uhvoR+DFqcNxs9vrRaBBSY3qda3Qe0bdud7tz2zGx1A==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.isnodelike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.isnodelike/-/utils.isnodelike-2.0.1.tgz", + "integrity": "sha512-w41XyepR+jBEuVpoRM715N2ZD0xMD413UiJx8w5xnAZD2ZkSJnMJBoIzauK83kJpSgNuR6ywbV29jG9NmxjK0Q==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.keyvaluecache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-2.1.1.tgz", + "integrity": "sha512-qVo5PvUUMD8oB9oYvq4ViCjYAMWnZ5zZwEjNF37L2m1u528x5mueMlU+Cr1UinupCgdB78g+egA1G98rbJ03Vw==", + "dependencies": { + "@apollo/utils.logger": "^2.0.1", + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-2.0.1.tgz", + "integrity": "sha512-YuplwLHaHf1oviidB7MxnCXAdHp3IqYV8n0momZ3JfLniae92eYqMIx+j5qJFX6WKJPs6q7bczmV4lXIsTu5Pg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.printwithreducedwhitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-2.0.1.tgz", + "integrity": "sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.removealiases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-2.0.1.tgz", + "integrity": "sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.sortast": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-2.0.1.tgz", + "integrity": "sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw==", + "dependencies": { + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.stripsensitiveliterals": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-2.0.1.tgz", + "integrity": "sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.usagereporting": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-2.1.0.tgz", + "integrity": "sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==", + "dependencies": { + "@apollo/usage-reporting-protobuf": "^4.1.0", + "@apollo/utils.dropunuseddefinitions": "^2.0.1", + "@apollo/utils.printwithreducedwhitespace": "^2.0.1", + "@apollo/utils.removealiases": "2.0.1", + "@apollo/utils.sortast": "^2.0.1", + "@apollo/utils.stripsensitiveliterals": "^2.0.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.withrequired": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-2.0.1.tgz", + "integrity": "sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@graphql-tools/merge": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", + "integrity": "sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==", + "dependencies": { + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", + "integrity": "sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==", + "dependencies": { + "@graphql-tools/merge": "^8.4.1", + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", + "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@josephg/resolvable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", + "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.2.tgz", + "integrity": "sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.1.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", + "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-graphviz": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-1.8.2.tgz", + "integrity": "sha512-5YhbFoHmjxa7pgQLkB07MtGnGJ/yhvjmc9uhsnDBEICME6gkPf83SBwLDQqGDoCa3XzUMWLk1AU2Wn1u1naDtA==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/examples/federation/package.json b/examples/federation/package.json new file mode 100644 index 0000000000..410141cfa2 --- /dev/null +++ b/examples/federation/package.json @@ -0,0 +1,18 @@ +{ + "name": "federation", + "version": "1.0.0", + "main": "gateway.js", + "type": "module", + "scripts": { + "start": "node gateway.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@apollo/gateway": "^2.8.4", + "@apollo/server": "^4.11.0", + "graphql": "^16.9.0" + } +} diff --git a/examples/federation/rover.sh b/examples/federation/rover.sh new file mode 100755 index 0000000000..aadf665b04 --- /dev/null +++ b/examples/federation/rover.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eumo pipefail + +function cleanup { + for pid in "${USER_ROVER_PID:-}"; do + # try kill all registered pids + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && kill "$pid" || echo "Could not kill $pid" + done +} +trap cleanup EXIT + +rover dev --url http://localhost:8001/graphql --name post & +sleep 1 +rover dev --url http://localhost:8002/graphql --name user & +USER_ROVER_PID=$! +sleep 1 +fg %1 diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index bcb21779c5..2eb5cf2714 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -40,7 +40,7 @@ directive @call( of the previous step is passed as input to the next step. """ steps: [Step] -) on FIELD_DEFINITION +) on FIELD_DEFINITION | OBJECT """ The `@expr` operators allows you to specify an expression that can evaluate to a @@ -48,7 +48,7 @@ value. The expression can be a static value or built form a Mustache template. s """ directive @expr( body: JSON -) on FIELD_DEFINITION +) on FIELD_DEFINITION | OBJECT """ The @graphQL operator allows to specify GraphQL API server request to fetch data @@ -82,7 +82,7 @@ directive @graphQL( field, Tailcall requests data from the corresponding upstream field. """ name: String! -) on FIELD_DEFINITION +) on FIELD_DEFINITION | OBJECT """ The @grpc operator indicates that a field or node is backed by a gRPC API.For instance, @@ -120,7 +120,7 @@ directive @grpc( This refers to the gRPC method you're going to call. For instance `GetAllNews`. """ method: String! -) on FIELD_DEFINITION +) on FIELD_DEFINITION | OBJECT """ The @http operator indicates that a field or node is backed by a REST API.For instance, @@ -190,11 +190,11 @@ directive @http( is automatically selected as the batching parameter. """ query: [URLQuery] -) on FIELD_DEFINITION +) on FIELD_DEFINITION | OBJECT directive @js( name: String! -) on FIELD_DEFINITION +) on FIELD_DEFINITION | OBJECT """ The @link directive allows you to import external resources, such as configuration diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index ef06397799..2261b63111 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -777,6 +777,18 @@ "title": "JSON", "description": "Field whose value conforms to the standard JSON format as specified in RFC 8259 (https://datatracker.ietf.org/doc/html/rfc8259)." }, + "Key": { + "description": "Directive `@key` for Apollo Federation", + "type": "object", + "required": [ + "fields" + ], + "properties": { + "fields": { + "type": "string" + } + } + }, "KeyValue": { "type": "object", "required": [ @@ -1245,6 +1257,80 @@ "Type": { "description": "Represents a GraphQL type. A type can be an object, interface, enum or scalar.", "type": "object", + "oneOf": [ + { + "type": "object", + "required": [ + "http" + ], + "properties": { + "http": { + "$ref": "#/definitions/Http" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "grpc" + ], + "properties": { + "grpc": { + "$ref": "#/definitions/Grpc" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "graphql" + ], + "properties": { + "graphql": { + "$ref": "#/definitions/GraphQL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "call" + ], + "properties": { + "call": { + "$ref": "#/definitions/Call" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "js" + ], + "properties": { + "js": { + "$ref": "#/definitions/JS" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "expr" + ], + "properties": { + "expr": { + "$ref": "#/definitions/Expr" + } + }, + "additionalProperties": false + } + ], "required": [ "fields" ], @@ -1289,6 +1375,18 @@ }, "uniqueItems": true }, + "key": { + "description": "Apollo federation key directive. skip since it's set automatically by config transformer", + "writeOnly": true, + "anyOf": [ + { + "$ref": "#/definitions/Key" + }, + { + "type": "null" + } + ] + }, "protected": { "description": "Marks field as protected by auth providers", "default": null, diff --git a/src/core/app_context.rs b/src/core/app_context.rs index 1e06f46b1d..fcc628a598 100644 --- a/src/core/app_context.rs +++ b/src/core/app_context.rs @@ -43,16 +43,18 @@ impl AppContext { for def in blueprint.definitions.iter_mut() { if let Definition::Object(def) = def { for field in &mut def.fields { - let of_type = field.of_type.clone(); let upstream_batch = &blueprint.upstream.batch; field.map_expr(|expr| { - expr.modify(|expr| match expr { + expr.modify(&mut |expr| match expr { IR::IO(io) => match io { - IO::Http { req_template, group_by, http_filter, .. } => { + IO::Http { + req_template, group_by, http_filter, is_list, .. + } => { + let is_list = *is_list; let data_loader = HttpDataLoader::new( runtime.clone(), group_by.clone(), - of_type.is_list(), + is_list, ) .to_data_loader(upstream_batch.clone().unwrap_or_default()); @@ -61,6 +63,7 @@ impl AppContext { group_by: group_by.clone(), dl_id: Some(DataLoaderId::new(http_data_loaders.len())), http_filter: http_filter.clone(), + is_list, })); http_data_loaders.push(data_loader); diff --git a/src/core/blueprint/definitions.rs b/src/core/blueprint/definitions.rs index 7a029afc08..c7e47650a9 100644 --- a/src/core/blueprint/definitions.rs +++ b/src/core/blueprint/definitions.rs @@ -492,6 +492,7 @@ pub fn to_field_definition( name: &String, ) -> Valid { update_args() + .and(update_apollo_federation(operation_type).trace("_entities")) .and(update_http().trace(config::Http::trace_name().as_str())) .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) .and(update_const_field().trace(config::Expr::trace_name().as_str())) diff --git a/src/core/blueprint/operators/apollo_federation.rs b/src/core/blueprint/operators/apollo_federation.rs new file mode 100644 index 0000000000..a078e6fe8c --- /dev/null +++ b/src/core/blueprint/operators/apollo_federation.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; +use std::fmt::Write; + +use async_graphql::parser::types::{SchemaDefinition, ServiceDocument, TypeSystemDefinition}; + +use super::{compile_call, compile_expr, compile_graphql, compile_grpc, compile_http, compile_js}; +use crate::core::blueprint::FieldDefinition; +use crate::core::config::{ + ApolloFederation, Config, ConfigModule, EntityResolver, Field, GraphQLOperationType, Resolver, +}; +use crate::core::ir::model::IR; +use crate::core::try_fold::TryFold; +use crate::core::valid::{Valid, Validator}; +use crate::core::{config, Type}; + +pub struct CompileEntityResolver<'a> { + config_module: &'a ConfigModule, + entity_resolver: &'a EntityResolver, + operation_type: &'a GraphQLOperationType, +} + +pub fn compile_entity_resolver(inputs: CompileEntityResolver<'_>) -> Valid { + let CompileEntityResolver { config_module, entity_resolver, operation_type } = inputs; + let mut resolver_by_type = HashMap::new(); + + Valid::from_iter( + entity_resolver.resolver_by_type.iter(), + |(type_name, resolver)| { + // Fake field that is required for validation in some cases + // TODO: should be a proper way to run the validation both + // on types and fields + let field = &Field { type_of: Type::from(type_name.clone()), ..Default::default() }; + + // TODO: make this code reusable in other operators like call + let ir = match resolver { + // TODO: there are `validate_field` for field, but not for types + // implement validation as shared entity and use it for types + Resolver::Http(http) => compile_http( + config_module, + http, + // inner resolver should resolve only single instance of type, not a list + false, + ), + Resolver::Grpc(grpc) => compile_grpc(super::CompileGrpc { + config_module, + operation_type, + field, + grpc, + validate_with_schema: true, + }), + Resolver::Graphql(graphql) => { + compile_graphql(config_module, operation_type, type_name, graphql) + } + Resolver::Call(call) => { + compile_call(config_module, call, operation_type, type_name) + } + Resolver::Js(js) => { + compile_js(super::CompileJs { js, script: &config_module.extensions().script }) + } + Resolver::Expr(expr) => { + compile_expr(super::CompileExpr { config_module, field, expr, validate: true }) + } + Resolver::ApolloFederation(federation) => match federation { + ApolloFederation::EntityResolver(entity_resolver) => { + compile_entity_resolver(CompileEntityResolver { entity_resolver, ..inputs }) + } + ApolloFederation::Service => Valid::fail( + "Apollo federation resolvers can't be a part of entity resolver" + .to_string(), + ), + }, + }; + + ir.map(|ir| { + resolver_by_type.insert(type_name.to_owned(), ir); + }) + }, + ) + .map_to(IR::Entity(resolver_by_type)) +} + +pub fn compile_service(config: &ConfigModule) -> Valid { + let mut sdl = + crate::core::document::print(filter_conflicting_directives(config.config().into())); + + writeln!(sdl).ok(); + // Add tailcall specific definitions to the sdl output + writeln!( + sdl, + "{}", + crate::core::document::print(filter_conflicting_directives(Config::graphql_schema())) + ) + .ok(); + writeln!(sdl).ok(); + // Mark subgraph as Apollo federation v2 compatible according to [docs](https://www.apollographql.com/docs/apollo-server/using-federation/apollo-subgraph-setup/#2-opt-in-to-federation-2) + // (borrowed from async_graphql) + writeln!(sdl, "extend schema @link(").ok(); + writeln!(sdl, "\turl: \"https://specs.apollo.dev/federation/v2.3\",").ok(); + writeln!(sdl, "\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]").ok(); + writeln!(sdl, ")").ok(); + + Valid::succeed(IR::Service(sdl)) +} + +fn filter_conflicting_directives(sd: ServiceDocument) -> ServiceDocument { + fn filter_directive(directive_name: &str) -> bool { + directive_name != "link" + } + + fn filter_map(def: TypeSystemDefinition) -> Option { + match def { + TypeSystemDefinition::Schema(schema) => { + Some(TypeSystemDefinition::Schema(schema.map(|schema| { + SchemaDefinition { + directives: schema + .directives + .into_iter() + .filter(|d| filter_directive(d.node.name.node.as_str())) + .collect(), + ..schema + } + }))) + } + TypeSystemDefinition::Directive(directive) => { + if filter_directive(directive.node.name.node.as_str()) { + Some(TypeSystemDefinition::Directive(directive)) + } else { + None + } + } + ty => Some(ty), + } + } + + ServiceDocument { + definitions: sd.definitions.into_iter().filter_map(filter_map).collect(), + } +} + +pub fn update_apollo_federation<'a>( + operation_type: &'a GraphQLOperationType, +) -> TryFold<'a, (&'a ConfigModule, &'a Field, &'a config::Type, &'a str), FieldDefinition, String> +{ + TryFold::<(&ConfigModule, &Field, &config::Type, &'a str), FieldDefinition, String>::new( + |(config_module, field, _, _), b_field| { + let Some(Resolver::ApolloFederation(federation)) = &field.resolver else { + return Valid::succeed(b_field); + }; + + match federation { + ApolloFederation::EntityResolver(entity_resolver) => { + compile_entity_resolver(CompileEntityResolver { + config_module, + entity_resolver, + operation_type, + }) + } + ApolloFederation::Service => compile_service(config_module), + } + .map(|resolver| b_field.resolver(Some(resolver))) + }, + ) +} diff --git a/src/core/blueprint/operators/call.rs b/src/core/blueprint/operators/call.rs index 3db52535b0..ebf8a2b361 100644 --- a/src/core/blueprint/operators/call.rs +++ b/src/core/blueprint/operators/call.rs @@ -13,23 +13,23 @@ pub fn update_call<'a>( ) -> TryFold<'a, (&'a ConfigModule, &'a Field, &'a config::Type, &'a str), FieldDefinition, String> { TryFold::<(&ConfigModule, &Field, &config::Type, &str), FieldDefinition, String>::new( - move |(config, field, _, name), b_field| { + move |(config, field, _, _), b_field| { let Some(Resolver::Call(call)) = &field.resolver else { return Valid::succeed(b_field); }; compile_call(config, call, operation_type, object_name) - .map(|field| b_field.resolver(field.resolver).name(name.to_string())) + .map(|resolver| b_field.resolver(Some(resolver))) }, ) } -fn compile_call( +pub fn compile_call( config_module: &ConfigModule, call: &config::Call, operation_type: &GraphQLOperationType, object_name: &str, -) -> Valid { +) -> Valid { Valid::from_iter(call.steps.iter(), |step| { get_field_and_field_name(step, config_module).and_then(|(field, field_name, type_of)| { let args = step.args.iter(); @@ -92,8 +92,6 @@ fn compile_call( .and_then(|b_fields| { Valid::from_option( b_fields.into_iter().reduce(|mut b_field, b_field_next| { - b_field.name = b_field_next.name; - b_field.of_type = b_field_next.of_type; b_field.map_expr(|expr| { b_field_next .resolver @@ -107,9 +105,14 @@ fn compile_call( "Steps can't be empty".to_string(), ) }) + .and_then(|field| { + Valid::from_option(field.resolver, "Result resolver can't be empty".to_string()) + }) } fn get_type_and_field(call: &config::Step) -> Option<(String, String)> { + // TODO: type names for query and mutations should be inferred from the + // config_module and should not be static values if let Some(query) = &call.query { Some(("Query".to_string(), query.clone())) } else { diff --git a/src/core/blueprint/operators/expr.rs b/src/core/blueprint/operators/expr.rs index de90d424b8..09dc6b7d7f 100644 --- a/src/core/blueprint/operators/expr.rs +++ b/src/core/blueprint/operators/expr.rs @@ -2,7 +2,7 @@ use async_graphql_value::ConstValue; use crate::core::blueprint::*; use crate::core::config; -use crate::core::config::{Field, Resolver}; +use crate::core::config::{Expr, Field, Resolver}; use crate::core::ir::model::IR; use crate::core::ir::model::IR::Dynamic; use crate::core::try_fold::TryFold; @@ -25,14 +25,14 @@ fn validate_data_with_schema( pub struct CompileExpr<'a> { pub config_module: &'a config::ConfigModule, pub field: &'a config::Field, - pub value: &'a serde_json::Value, + pub expr: &'a Expr, pub validate: bool, } pub fn compile_expr(inputs: CompileExpr) -> Valid { let config_module = inputs.config_module; let field = inputs.field; - let value = inputs.value; + let value = &inputs.expr.body; let validate = inputs.validate; Valid::from( @@ -68,7 +68,7 @@ pub fn update_const_field<'a>( return Valid::succeed(b_field); }; - compile_expr(CompileExpr { config_module, field, value: &expr.body, validate: true }) + compile_expr(CompileExpr { config_module, field, expr, validate: true }) .map(|resolver| b_field.resolver(Some(resolver))) }, ) diff --git a/src/core/blueprint/operators/graphql.rs b/src/core/blueprint/operators/graphql.rs index 0c7fba35af..394b1ca524 100644 --- a/src/core/blueprint/operators/graphql.rs +++ b/src/core/blueprint/operators/graphql.rs @@ -41,7 +41,7 @@ fn create_related_fields( } pub fn compile_graphql( - config: &Config, + config: &ConfigModule, operation_type: &GraphQLOperationType, type_name: &str, graphql: &GraphQL, diff --git a/src/core/blueprint/operators/http.rs b/src/core/blueprint/operators/http.rs index 4db8a2ae02..62ff58f2e1 100644 --- a/src/core/blueprint/operators/http.rs +++ b/src/core/blueprint/operators/http.rs @@ -11,6 +11,7 @@ use crate::core::{config, helpers, Mustache}; pub fn compile_http( config_module: &config::ConfigModule, http: &config::Http, + is_list: bool, ) -> Valid { Valid::<(), String>::fail("GroupBy is only supported for GET requests".to_string()) .when(|| !http.batch_key.is_empty() && http.method != Method::GET) @@ -79,9 +80,16 @@ pub fn compile_http( group_by: Some(GroupBy::new(http.batch_key.clone(), key)), dl_id: None, http_filter, + is_list, }) } else { - IR::IO(IO::Http { req_template, group_by: None, dl_id: None, http_filter }) + IR::IO(IO::Http { + req_template, + group_by: None, + dl_id: None, + http_filter, + is_list, + }) } }) } @@ -95,7 +103,7 @@ pub fn update_http<'a>( return Valid::succeed(b_field); }; - compile_http(config_module, http) + compile_http(config_module, http, field.type_of.is_list()) .map(|resolver| b_field.resolver(Some(resolver))) .and_then(|b_field| { b_field diff --git a/src/core/blueprint/operators/js.rs b/src/core/blueprint/operators/js.rs index cc6b212c60..cf61d20e19 100644 --- a/src/core/blueprint/operators/js.rs +++ b/src/core/blueprint/operators/js.rs @@ -1,17 +1,17 @@ use crate::core::blueprint::FieldDefinition; use crate::core::config; -use crate::core::config::{ConfigModule, Field, Resolver}; +use crate::core::config::{ConfigModule, Field, Resolver, JS}; use crate::core::ir::model::{IO, IR}; use crate::core::try_fold::TryFold; use crate::core::valid::{Valid, Validator}; pub struct CompileJs<'a> { - pub name: &'a str, + pub js: &'a JS, pub script: &'a Option, } pub fn compile_js(inputs: CompileJs) -> Valid { - let name = inputs.name; + let name = &inputs.js.name; Valid::from_option(inputs.script.as_ref(), "script is required".to_string()) .map(|_| IR::IO(IO::Js { name: name.to_string() })) } @@ -25,7 +25,7 @@ pub fn update_js_field<'a>( return Valid::succeed(b_field); }; - compile_js(CompileJs { script: &module.extensions().script, name: &js.name }) + compile_js(CompileJs { script: &module.extensions().script, js }) .map(|resolver| b_field.resolver(Some(resolver))) }, ) diff --git a/src/core/blueprint/operators/mod.rs b/src/core/blueprint/operators/mod.rs index 7ca9bf0e1a..2b4842899e 100644 --- a/src/core/blueprint/operators/mod.rs +++ b/src/core/blueprint/operators/mod.rs @@ -1,3 +1,4 @@ +mod apollo_federation; mod call; mod enum_alias; mod expr; @@ -8,6 +9,7 @@ mod js; mod modify; mod protected; +pub use apollo_federation::*; pub use call::*; pub use enum_alias::*; pub use expr::*; diff --git a/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap b/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap index fc441a38e2..1f46ab6ccc 100644 --- a/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap +++ b/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap @@ -69,6 +69,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: false, }, ), ), @@ -136,6 +137,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: false, }, ), ), @@ -211,6 +213,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: false, }, ), ), @@ -290,6 +293,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: false, }, ), ), @@ -685,6 +689,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: true, }, ), ), @@ -746,6 +751,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: false, }, ), ), @@ -844,6 +850,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: true, }, ), ), @@ -917,6 +924,7 @@ Index { group_by: None, dl_id: None, http_filter: None, + is_list: false, }, ), ), diff --git a/src/core/config/config.rs b/src/core/config/config.rs index 952de581a3..d04c0f3fe8 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -3,27 +3,23 @@ use std::fmt::{self, Display}; use std::num::NonZeroU64; use anyhow::Result; -use async_graphql::parser::types::{ConstDirective, ServiceDocument}; -use async_graphql::Positioned; +use async_graphql::parser::types::ServiceDocument; use derive_setters::Setters; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::Value; -use tailcall_macros::{CustomResolver, DirectiveDefinition, InputDefinition}; +use tailcall_macros::{DirectiveDefinition, InputDefinition}; use tailcall_typedefs_common::directive_definition::DirectiveDefinition; use tailcall_typedefs_common::input_definition::InputDefinition; use tailcall_typedefs_common::ServiceDocumentBuilder; +use super::directives::{Call, Expr, GraphQL, Grpc, Http, Key, JS}; +use super::from_document::from_document; use super::telemetry::Telemetry; -use super::{KeyValue, Link, Server, Upstream}; -use crate::core::config::from_document::from_document; +use super::{Link, Resolver, Server, Upstream}; use crate::core::config::npo::QueryPath; use crate::core::config::source::Source; -use crate::core::config::url_query::URLQuery; -use crate::core::directive::DirectiveCodec; -use crate::core::http::Method; use crate::core::is_default; -use crate::core::json::JsonSchema; use crate::core::macros::MergeRight; use crate::core::merge_right::MergeRight; use crate::core::scalar::Scalar; @@ -116,6 +112,17 @@ pub struct Type { /// Marks field as protected by auth providers #[serde(default)] pub protected: Option, + + /// + /// Apollo federation entity resolver. + #[serde(flatten, default, skip_serializing_if = "is_default")] + pub resolver: Option, + + /// + /// Apollo federation key directive. + /// skip since it's set automatically by config transformer + #[serde(skip_serializing)] + pub key: Option, } impl Display for Type { @@ -211,19 +218,6 @@ pub struct RootSchema { /// Used to omit a field from public consumption. pub struct Omit {} -#[derive( - Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, CustomResolver, -)] -#[serde(rename_all = "camelCase")] -pub enum Resolver { - Http(Http), - Grpc(Grpc), - Graphql(GraphQL), - Call(Call), - Js(JS), - Expr(Expr), -} - /// /// A field definition containing all the metadata information about resolving a /// field. @@ -289,19 +283,12 @@ impl Field { pub fn has_resolver(&self) -> bool { self.resolver.is_some() } + pub fn has_batched_resolver(&self) -> bool { - if let Some(resolver) = &self.resolver { - match resolver { - Resolver::Http(http) => !http.batch_key.is_empty(), - Resolver::Grpc(grpc) => !grpc.batch_key.is_empty(), - Resolver::Graphql(graphql) => graphql.batch, - Resolver::Call(_) => false, - Resolver::Js(_) => false, - Resolver::Expr(_) => false, - } - } else { - false - } + self.resolver + .as_ref() + .map(Resolver::is_batched) + .unwrap_or(false) } pub fn int() -> Self { @@ -334,22 +321,6 @@ impl Field { } } -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - PartialEq, - Eq, - schemars::JsonSchema, - DirectiveDefinition, - InputDefinition, -)] -#[directive_definition(locations = "FieldDefinition", lowercase_name)] -pub struct JS { - pub name: String, -} - #[derive( Serialize, Deserialize, @@ -440,228 +411,6 @@ pub struct Alias { pub options: BTreeSet, } -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - Default, - PartialEq, - Eq, - schemars::JsonSchema, - DirectiveDefinition, - InputDefinition, -)] -#[directive_definition(locations = "FieldDefinition")] -#[serde(deny_unknown_fields)] -/// The @http operator indicates that a field or node is backed by a REST API. -/// -/// For instance, if you add the @http operator to the `users` field of the -/// Query type with a path argument of `"/users"`, it signifies that the `users` -/// field is backed by a REST API. The path argument specifies the path of the -/// REST API. In this scenario, the GraphQL server will make a GET request to -/// the API endpoint specified when the `users` field is queried. -pub struct Http { - #[serde(rename = "onRequest", default, skip_serializing_if = "is_default")] - /// onRequest field in @http directive gives the ability to specify the - /// request interception handler. - pub on_request: Option, - - #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")] - /// This refers to the base URL of the API. If not specified, the default - /// base URL is the one specified in the `@upstream` operator. - pub base_url: Option, - - #[serde(default, skip_serializing_if = "is_default")] - /// The body of the API call. It's used for methods like POST or PUT that - /// send data to the server. You can pass it as a static object or use a - /// Mustache template to substitute variables from the GraphQL variables. - pub body: Option, - - #[serde(default, skip_serializing_if = "is_default")] - /// The `encoding` parameter specifies the encoding of the request body. It - /// can be `ApplicationJson` or `ApplicationXWwwFormUrlEncoded`. @default - /// `ApplicationJson`. - pub encoding: Encoding, - - #[serde(rename = "batchKey", default, skip_serializing_if = "is_default")] - /// The `batchKey` dictates the path Tailcall will follow to group the returned items from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching). - pub batch_key: Vec, - - #[serde(default, skip_serializing_if = "is_default")] - /// The `headers` parameter allows you to customize the headers of the HTTP - /// request made by the `@http` operator. It is used by specifying a - /// key-value map of header names and their values. - pub headers: Vec, - - #[serde(default, skip_serializing_if = "is_default")] - /// Schema of the input of the API call. It is automatically inferred in - /// most cases. - pub input: Option, - - #[serde(default, skip_serializing_if = "is_default")] - /// This refers to the HTTP method of the API call. Commonly used methods - /// include `GET`, `POST`, `PUT`, `DELETE` etc. @default `GET`. - pub method: Method, - - /// This refers to the API endpoint you're going to call. For instance `https://jsonplaceholder.typicode.com/users`. - /// - /// For dynamic segments in your API endpoint, use Mustache templates for - /// variable substitution. For instance, to fetch a specific user, use - /// `/users/{{args.id}}`. - pub path: String, - - #[serde(default, skip_serializing_if = "is_default")] - /// Schema of the output of the API call. It is automatically inferred in - /// most cases. - pub output: Option, - - #[serde(default, skip_serializing_if = "is_default")] - /// This represents the query parameters of your API call. You can pass it - /// as a static object or use Mustache template for dynamic parameters. - /// These parameters will be added to the URL. - /// NOTE: Query parameter order is critical for batching in Tailcall. The - /// first parameter referencing a field in the current value using mustache - /// syntax is automatically selected as the batching parameter. - pub query: Vec, -} - -/// -/// Provides the ability to refer to multiple fields in the Query or -/// Mutation root. -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - Default, - PartialEq, - Eq, - schemars::JsonSchema, - DirectiveDefinition, -)] -#[directive_definition(locations = "FieldDefinition")] -pub struct Call { - /// Steps are composed together to form a call. - /// If you have multiple steps, the output of the previous step is passed as - /// input to the next step. - pub steps: Vec, -} - -/// -/// Provides the ability to refer to a field defined in the root Query or -/// Mutation. -#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema)] -pub struct Step { - #[serde(default, skip_serializing_if = "is_default")] - /// The name of the field on the `Query` type that you want to call. - pub query: Option, - - #[serde(default, skip_serializing_if = "is_default")] - /// The name of the field on the `Mutation` type that you want to call. - pub mutation: Option, - - /// The arguments that will override the actual arguments of the field. - #[serde(default, skip_serializing_if = "is_default")] - pub args: BTreeMap, -} - -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - Default, - PartialEq, - Eq, - schemars::JsonSchema, - InputDefinition, - DirectiveDefinition, -)] -#[directive_definition(locations = "FieldDefinition")] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -/// The @grpc operator indicates that a field or node is backed by a gRPC API. -/// -/// For instance, if you add the @grpc operator to the `users` field of the -/// Query type with a service argument of `NewsService` and method argument of -/// `GetAllNews`, it signifies that the `users` field is backed by a gRPC API. -/// The `service` argument specifies the name of the gRPC service. -/// The `method` argument specifies the name of the gRPC method. -/// In this scenario, the GraphQL server will make a gRPC request to the gRPC -/// endpoint specified when the `users` field is queried. -pub struct Grpc { - #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")] - /// This refers to the base URL of the API. If not specified, the default - /// base URL is the one specified in the `@upstream` operator. - pub base_url: Option, - #[serde(default, skip_serializing_if = "is_default")] - /// This refers to the arguments of your gRPC call. You can pass it as a - /// static object or use Mustache template for dynamic parameters. These - /// parameters will be added in the body in `protobuf` format. - pub body: Option, - #[serde(rename = "batchKey", default, skip_serializing_if = "is_default")] - /// The `batchKey` dictates the path Tailcall will follow to group the returned items from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching). - pub batch_key: Vec, - #[serde(default, skip_serializing_if = "is_default")] - /// The `headers` parameter allows you to customize the headers of the HTTP - /// request made by the `@grpc` operator. It is used by specifying a - /// key-value map of header names and their values. Note: content-type is - /// automatically set to application/grpc - pub headers: Vec, - /// This refers to the gRPC method you're going to call. For instance - /// `GetAllNews`. - pub method: String, -} - -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - Default, - PartialEq, - Eq, - schemars::JsonSchema, - DirectiveDefinition, - InputDefinition, -)] -#[directive_definition(locations = "FieldDefinition")] -#[serde(deny_unknown_fields)] -/// The @graphQL operator allows to specify GraphQL API server request to fetch -/// data from. -pub struct GraphQL { - #[serde(default, skip_serializing_if = "is_default")] - /// Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args) - pub args: Option>, - - #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")] - /// This refers to the base URL of the API. If not specified, the default - /// base URL is the one specified in the `@upstream` operator. - pub base_url: Option, - - #[serde(default, skip_serializing_if = "is_default")] - /// If the upstream GraphQL server supports request batching, you can - /// specify the 'batch' argument to batch several requests into a single - /// batch request. - /// - /// Make sure you have also specified batch settings to the `@upstream` and - /// to the `@graphQL` operator. - pub batch: bool, - - #[serde(default, skip_serializing_if = "is_default")] - /// The headers parameter allows you to customize the headers of the GraphQL - /// request made by the `@graphQL` operator. It is used by specifying a - /// key-value map of header names and their values. - pub headers: Vec, - - /// Specifies the root field on the upstream to request data from. This maps - /// a field in your schema to a field in the upstream schema. When a query - /// is received for this field, Tailcall requests data from the - /// corresponding upstream field. - pub name: String, -} - #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum GraphQLOperationType { @@ -679,26 +428,6 @@ impl Display for GraphQLOperationType { } } -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - PartialEq, - Eq, - schemars::JsonSchema, - DirectiveDefinition, - InputDefinition, -)] -#[directive_definition(locations = "FieldDefinition")] -#[serde(deny_unknown_fields)] -/// The `@expr` operators allows you to specify an expression that can evaluate -/// to a value. The expression can be a static value or built form a Mustache -/// template. schema. -pub struct Expr { - pub body: Value, -} - #[derive( Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, DirectiveDefinition, )] @@ -1017,6 +746,7 @@ mod tests { use pretty_assertions::assert_eq; use super::*; + use crate::core::directive::DirectiveCodec; #[test] fn test_field_has_or_not_batch_resolver() { diff --git a/src/core/config/directives/call.rs b/src/core/config/directives/call.rs new file mode 100644 index 0000000000..250f7bebf2 --- /dev/null +++ b/src/core/config/directives/call.rs @@ -0,0 +1,47 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tailcall_macros::DirectiveDefinition; + +use crate::core::is_default; + +/// +/// Provides the ability to refer to a field defined in the root Query or +/// Mutation. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema)] +pub struct Step { + #[serde(default, skip_serializing_if = "is_default")] + /// The name of the field on the `Query` type that you want to call. + pub query: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// The name of the field on the `Mutation` type that you want to call. + pub mutation: Option, + + /// The arguments that will override the actual arguments of the field. + #[serde(default, skip_serializing_if = "is_default")] + pub args: BTreeMap, +} + +/// +/// Provides the ability to refer to multiple fields in the Query or +/// Mutation root. +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + Default, + PartialEq, + Eq, + schemars::JsonSchema, + DirectiveDefinition, +)] +#[directive_definition(locations = "FieldDefinition, Object")] +pub struct Call { + /// Steps are composed together to form a call. + /// If you have multiple steps, the output of the previous step is passed as + /// input to the next step. + pub steps: Vec, +} diff --git a/src/core/config/directives/expr.rs b/src/core/config/directives/expr.rs new file mode 100644 index 0000000000..39dcced1ce --- /dev/null +++ b/src/core/config/directives/expr.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tailcall_macros::{DirectiveDefinition, InputDefinition}; + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + schemars::JsonSchema, + DirectiveDefinition, + InputDefinition, +)] +#[directive_definition(locations = "FieldDefinition, Object")] +#[serde(deny_unknown_fields)] +/// The `@expr` operators allows you to specify an expression that can evaluate +/// to a value. The expression can be a static value or built form a Mustache +/// template. schema. +pub struct Expr { + pub body: Value, +} diff --git a/src/core/config/directives/federation.rs b/src/core/config/directives/federation.rs new file mode 100644 index 0000000000..d956e9b6b6 --- /dev/null +++ b/src/core/config/directives/federation.rs @@ -0,0 +1,21 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use tailcall_macros::MergeRight; + +use crate::core::config::Resolver; +use crate::core::merge_right::MergeRight; + +/// Directive `@key` for Apollo Federation +#[derive( + Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema, MergeRight, +)] +pub struct Key { + pub fields: String, +} + +/// Resolver for `_entities` field for Apollo Federation +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct EntityResolver { + pub resolver_by_type: BTreeMap, +} diff --git a/src/core/config/directives/graphql.rs b/src/core/config/directives/graphql.rs new file mode 100644 index 0000000000..e9bef555d1 --- /dev/null +++ b/src/core/config/directives/graphql.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use tailcall_macros::{DirectiveDefinition, InputDefinition}; + +use crate::core::config::KeyValue; +use crate::core::is_default; + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + Default, + PartialEq, + Eq, + schemars::JsonSchema, + DirectiveDefinition, + InputDefinition, +)] +#[directive_definition(locations = "FieldDefinition, Object")] +#[serde(deny_unknown_fields)] +/// The @graphQL operator allows to specify GraphQL API server request to fetch +/// data from. +pub struct GraphQL { + #[serde(default, skip_serializing_if = "is_default")] + /// Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args) + pub args: Option>, + + #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")] + /// This refers to the base URL of the API. If not specified, the default + /// base URL is the one specified in the `@upstream` operator. + pub base_url: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// If the upstream GraphQL server supports request batching, you can + /// specify the 'batch' argument to batch several requests into a single + /// batch request. + /// + /// Make sure you have also specified batch settings to the `@upstream` and + /// to the `@graphQL` operator. + pub batch: bool, + + #[serde(default, skip_serializing_if = "is_default")] + /// The headers parameter allows you to customize the headers of the GraphQL + /// request made by the `@graphQL` operator. It is used by specifying a + /// key-value map of header names and their values. + pub headers: Vec, + + /// Specifies the root field on the upstream to request data from. This maps + /// a field in your schema to a field in the upstream schema. When a query + /// is received for this field, Tailcall requests data from the + /// corresponding upstream field. + pub name: String, +} diff --git a/src/core/config/directives/grpc.rs b/src/core/config/directives/grpc.rs new file mode 100644 index 0000000000..9ee9a6a126 --- /dev/null +++ b/src/core/config/directives/grpc.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tailcall_macros::{DirectiveDefinition, InputDefinition}; + +use crate::core::config::KeyValue; +use crate::core::is_default; + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + Default, + PartialEq, + Eq, + schemars::JsonSchema, + InputDefinition, + DirectiveDefinition, +)] +#[directive_definition(locations = "FieldDefinition, Object")] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +/// The @grpc operator indicates that a field or node is backed by a gRPC API. +/// +/// For instance, if you add the @grpc operator to the `users` field of the +/// Query type with a service argument of `NewsService` and method argument of +/// `GetAllNews`, it signifies that the `users` field is backed by a gRPC API. +/// The `service` argument specifies the name of the gRPC service. +/// The `method` argument specifies the name of the gRPC method. +/// In this scenario, the GraphQL server will make a gRPC request to the gRPC +/// endpoint specified when the `users` field is queried. +pub struct Grpc { + #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")] + /// This refers to the base URL of the API. If not specified, the default + /// base URL is the one specified in the `@upstream` operator. + pub base_url: Option, + #[serde(default, skip_serializing_if = "is_default")] + /// This refers to the arguments of your gRPC call. You can pass it as a + /// static object or use Mustache template for dynamic parameters. These + /// parameters will be added in the body in `protobuf` format. + pub body: Option, + #[serde(rename = "batchKey", default, skip_serializing_if = "is_default")] + /// The `batchKey` dictates the path Tailcall will follow to group the returned items from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching). + pub batch_key: Vec, + #[serde(default, skip_serializing_if = "is_default")] + /// The `headers` parameter allows you to customize the headers of the HTTP + /// request made by the `@grpc` operator. It is used by specifying a + /// key-value map of header names and their values. Note: content-type is + /// automatically set to application/grpc + pub headers: Vec, + /// This refers to the gRPC method you're going to call. For instance + /// `GetAllNews`. + pub method: String, +} diff --git a/src/core/config/directives/http.rs b/src/core/config/directives/http.rs new file mode 100644 index 0000000000..35e4ff3ca4 --- /dev/null +++ b/src/core/config/directives/http.rs @@ -0,0 +1,93 @@ +use serde::{Deserialize, Serialize}; +use tailcall_macros::{DirectiveDefinition, InputDefinition}; + +use crate::core::config::{Encoding, KeyValue, URLQuery}; +use crate::core::http::Method; +use crate::core::is_default; +use crate::core::json::JsonSchema; + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + Default, + PartialEq, + Eq, + schemars::JsonSchema, + DirectiveDefinition, + InputDefinition, +)] +#[directive_definition(locations = "FieldDefinition, Object")] +#[serde(deny_unknown_fields)] +/// The @http operator indicates that a field or node is backed by a REST API. +/// +/// For instance, if you add the @http operator to the `users` field of the +/// Query type with a path argument of `"/users"`, it signifies that the `users` +/// field is backed by a REST API. The path argument specifies the path of the +/// REST API. In this scenario, the GraphQL server will make a GET request to +/// the API endpoint specified when the `users` field is queried. +pub struct Http { + #[serde(rename = "onRequest", default, skip_serializing_if = "is_default")] + /// onRequest field in @http directive gives the ability to specify the + /// request interception handler. + pub on_request: Option, + + #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")] + /// This refers to the base URL of the API. If not specified, the default + /// base URL is the one specified in the `@upstream` operator. + pub base_url: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// The body of the API call. It's used for methods like POST or PUT that + /// send data to the server. You can pass it as a static object or use a + /// Mustache template to substitute variables from the GraphQL variables. + pub body: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// The `encoding` parameter specifies the encoding of the request body. It + /// can be `ApplicationJson` or `ApplicationXWwwFormUrlEncoded`. @default + /// `ApplicationJson`. + pub encoding: Encoding, + + #[serde(rename = "batchKey", default, skip_serializing_if = "is_default")] + /// The `batchKey` dictates the path Tailcall will follow to group the returned items from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching). + pub batch_key: Vec, + + #[serde(default, skip_serializing_if = "is_default")] + /// The `headers` parameter allows you to customize the headers of the HTTP + /// request made by the `@http` operator. It is used by specifying a + /// key-value map of header names and their values. + pub headers: Vec, + + #[serde(default, skip_serializing_if = "is_default")] + /// Schema of the input of the API call. It is automatically inferred in + /// most cases. + pub input: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// This refers to the HTTP method of the API call. Commonly used methods + /// include `GET`, `POST`, `PUT`, `DELETE` etc. @default `GET`. + pub method: Method, + + /// This refers to the API endpoint you're going to call. For instance `https://jsonplaceholder.typicode.com/users`. + /// + /// For dynamic segments in your API endpoint, use Mustache templates for + /// variable substitution. For instance, to fetch a specific user, use + /// `/users/{{args.id}}`. + pub path: String, + + #[serde(default, skip_serializing_if = "is_default")] + /// Schema of the output of the API call. It is automatically inferred in + /// most cases. + pub output: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// This represents the query parameters of your API call. You can pass it + /// as a static object or use Mustache template for dynamic parameters. + /// These parameters will be added to the URL. + /// NOTE: Query parameter order is critical for batching in Tailcall. The + /// first parameter referencing a field in the current value using mustache + /// syntax is automatically selected as the batching parameter. + pub query: Vec, +} diff --git a/src/core/config/directives/js.rs b/src/core/config/directives/js.rs new file mode 100644 index 0000000000..60f307befc --- /dev/null +++ b/src/core/config/directives/js.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; +use tailcall_macros::{DirectiveDefinition, InputDefinition}; + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + schemars::JsonSchema, + DirectiveDefinition, + InputDefinition, +)] +#[directive_definition(locations = "FieldDefinition, Object", lowercase_name)] +pub struct JS { + pub name: String, +} diff --git a/src/core/config/directives/mod.rs b/src/core/config/directives/mod.rs new file mode 100644 index 0000000000..820286d2c3 --- /dev/null +++ b/src/core/config/directives/mod.rs @@ -0,0 +1,15 @@ +mod call; +mod expr; +mod federation; +mod graphql; +mod grpc; +mod http; +mod js; + +pub use call::*; +pub use expr::*; +pub use federation::*; +pub use graphql::*; +pub use grpc::*; +pub use http::*; +pub use js::*; diff --git a/src/core/config/from_document.rs b/src/core/config/from_document.rs index 3fa2af4a06..50ee61153e 100644 --- a/src/core/config/from_document.rs +++ b/src/core/config/from_document.rs @@ -11,7 +11,7 @@ use async_graphql_value::ConstValue; use indexmap::IndexMap; use super::telemetry::Telemetry; -use super::Alias; +use super::{Alias, Resolver}; use crate::core::config::{ self, Cache, Config, Enum, Link, Modify, Omit, Protected, RootSchema, Server, Union, Upstream, Variant, @@ -242,14 +242,24 @@ where let fields = object.fields(); let implements = object.implements(); - Cache::from_directives(directives.iter()) + Resolver::from_directives(directives) + .fuse(Cache::from_directives(directives.iter())) .fuse(to_fields(fields)) .fuse(Protected::from_directives(directives.iter())) - .map(|(cache, fields, protected)| { + .map(|(resolver, cache, fields, protected)| { let doc = description.to_owned().map(|pos| pos.node); let implements = implements.iter().map(|pos| pos.node.to_string()).collect(); let added_fields = to_add_fields_from_directives(directives); - config::Type { fields, added_fields, doc, implements, cache, protected } + config::Type { + fields, + added_fields, + doc, + implements, + cache, + protected, + resolver, + key: None, + } }) } fn to_input_object( diff --git a/src/core/config/into_document.rs b/src/core/config/into_document.rs index 203c007338..1b1db9e23d 100644 --- a/src/core/config/into_document.rs +++ b/src/core/config/into_document.rs @@ -150,27 +150,38 @@ fn config_document(config: &Config) -> ServiceDocument { .collect::>>(), }) }; + + let directives = type_def + .added_fields + .iter() + .map(|added_field: &super::AddField| pos(added_field.to_directive())) + .chain( + type_def + .cache + .as_ref() + .map(|cache| pos(cache.to_directive())), + ) + .chain( + type_def + .protected + .as_ref() + .map(|protected| pos(protected.to_directive())), + ) + .chain( + type_def + .resolver + .as_ref() + .and_then(|resolver| resolver.to_directive()) + .map(pos), + ) + .chain(type_def.key.as_ref().map(|key| pos(key.to_directive()))) + .collect::>(); + definitions.push(TypeSystemDefinition::Type(pos(TypeDefinition { extend: false, description: type_def.doc.clone().map(pos), name: pos(Name::new(type_name.clone())), - directives: type_def - .added_fields - .iter() - .map(|added_field: &super::AddField| pos(added_field.to_directive())) - .chain( - type_def - .cache - .as_ref() - .map(|cache| pos(cache.to_directive())), - ) - .chain( - type_def - .protected - .as_ref() - .map(|protected| pos(protected.to_directive())), - ) - .collect::>(), + directives, kind, }))); } @@ -220,7 +231,11 @@ fn config_document(config: &Config) -> ServiceDocument { fn get_directives(field: &crate::core::config::Field) -> Vec> { let directives = vec![ - field.resolver.as_ref().map(|d| pos(d.to_directive())), + field + .resolver + .as_ref() + .and_then(|d| d.to_directive()) + .map(pos), field.modify.as_ref().map(|d| pos(d.to_directive())), field.omit.as_ref().map(|d| pos(d.to_directive())), field.cache.as_ref().map(|d| pos(d.to_directive())), diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 6135656368..8288d9485c 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1,10 +1,12 @@ pub use apollo::*; pub use config::*; pub use config_module::*; +pub use directives::*; pub use key_values::*; pub use link::*; pub use npo::QueryPath; pub use reader_context::*; +pub use resolver::*; pub use server::*; pub use source::*; pub use telemetry::*; @@ -14,6 +16,7 @@ mod apollo; mod config; mod config_module; pub mod cors; +pub mod directives; mod from_document; pub mod group_by; mod headers; @@ -23,6 +26,7 @@ mod link; mod npo; pub mod reader; pub mod reader_context; +mod resolver; mod server; mod source; mod telemetry; diff --git a/src/core/config/resolver.rs b/src/core/config/resolver.rs new file mode 100644 index 0000000000..c8b42c21a0 --- /dev/null +++ b/src/core/config/resolver.rs @@ -0,0 +1,56 @@ +use async_graphql::parser::types::ConstDirective; +use async_graphql::Positioned; +use serde::{Deserialize, Serialize}; +use tailcall_macros::{CustomResolver, MergeRight}; + +use super::{Call, EntityResolver, Expr, GraphQL, Grpc, Http, JS}; +use crate::core::directive::DirectiveCodec; +use crate::core::merge_right::MergeRight; +use crate::core::valid::{Valid, Validator}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ApolloFederation { + EntityResolver(EntityResolver), + Service, +} + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + schemars::JsonSchema, + MergeRight, + CustomResolver, +)] +#[serde(rename_all = "camelCase")] +pub enum Resolver { + Http(Http), + Grpc(Grpc), + Graphql(GraphQL), + Call(Call), + Js(JS), + Expr(Expr), + #[serde(skip)] + #[resolver(skip_directive)] + ApolloFederation(ApolloFederation), +} + +impl Resolver { + pub fn is_batched(&self) -> bool { + match self { + Resolver::Http(http) => !http.batch_key.is_empty(), + Resolver::Grpc(grpc) => !grpc.batch_key.is_empty(), + Resolver::Graphql(graphql) => graphql.batch, + Resolver::ApolloFederation(ApolloFederation::EntityResolver(entity_resolver)) => { + entity_resolver + .resolver_by_type + .values() + .any(Resolver::is_batched) + } + _ => false, + } + } +} diff --git a/src/core/config/transformer/mod.rs b/src/core/config/transformer/mod.rs index bf22d6730c..75a1396761 100644 --- a/src/core/config/transformer/mod.rs +++ b/src/core/config/transformer/mod.rs @@ -7,6 +7,7 @@ mod nested_unions; mod preset; mod rename_types; mod required; +mod subgraph; mod tree_shake; mod union_input_type; @@ -19,5 +20,6 @@ pub use nested_unions::NestedUnions; pub use preset::Preset; pub use rename_types::RenameTypes; pub use required::Required; +pub use subgraph::Subgraph; pub use tree_shake::TreeShake; pub use union_input_type::UnionInputType; diff --git a/src/core/config/transformer/required.rs b/src/core/config/transformer/required.rs index 4d38070a5c..e8ba0c2803 100644 --- a/src/core/config/transformer/required.rs +++ b/src/core/config/transformer/required.rs @@ -15,6 +15,7 @@ impl Transform for Required { config: Self::Value, ) -> crate::core::valid::Valid { transform::default() + .pipe(super::Subgraph) .pipe(super::NestedUnions) .pipe(super::UnionInputType) .pipe(super::AmbiguousType::default()) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_call.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_call.snap new file mode 100644 index 0000000000..056418f3e3 --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_call.snap @@ -0,0 +1,11 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Valid( + Ok( + Some( + "arg { a b }", + ), + ), +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_expr.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_expr.snap new file mode 100644 index 0000000000..29c06be101 --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_expr.snap @@ -0,0 +1,11 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Valid( + Ok( + Some( + "body { a b }", + ), + ), +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_graphql.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_graphql.snap new file mode 100644 index 0000000000..ae0c75e46a --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_graphql.snap @@ -0,0 +1,11 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Valid( + Ok( + Some( + "header_test input { key }", + ), + ), +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_grpc.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_grpc.snap new file mode 100644 index 0000000000..a58bbbb93b --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_grpc.snap @@ -0,0 +1,11 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Valid( + Ok( + Some( + "body { a b } header_test method", + ), + ), +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_http.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_http.snap new file mode 100644 index 0000000000..bd776f6c9d --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__extract_http.snap @@ -0,0 +1,11 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Valid( + Ok( + Some( + "header { key value } id obj query_key query_value", + ), + ), +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__non_value_template.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__non_value_template.snap new file mode 100644 index 0000000000..8fabb5a9e6 --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__extractor__non_value_template.snap @@ -0,0 +1,28 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Valid( + Err( + ValidationError( + [ + Cause { + message: "Type resolver can't use `.args`, use `.value` instead", + description: None, + trace: [ + "http", + "path", + ], + }, + Cause { + message: "Type resolver can't use `.args`, use `.value` instead", + description: None, + trace: [ + "http", + "query", + ], + }, + ], + ), + ), +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_merge.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_merge.snap new file mode 100644 index 0000000000..a1a1125ffd --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_merge.snap @@ -0,0 +1,32 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys_a.merge_right(keys_b) +--- +Keys( + { + "a": Keys( + { + "1": Keys( + {}, + ), + "b": Keys( + {}, + ), + }, + ), + "c": Keys( + { + "2": Keys( + {}, + ), + }, + ), + "d": Keys( + { + "3": Keys( + {}, + ), + }, + ), + }, +) diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_set.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_set.snap new file mode 100644 index 0000000000..0ca1900d8e --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__subgraph__tests__keys__keys_set.snap @@ -0,0 +1,32 @@ +--- +source: src/core/config/transformer/subgraph.rs +expression: keys +--- +Keys( + { + "a": Keys( + { + "b": Keys( + { + "c": Keys( + {}, + ), + }, + ), + "d": Keys( + {}, + ), + }, + ), + "e": Keys( + {}, + ), + "f": Keys( + { + "g": Keys( + {}, + ), + }, + ), + }, +) diff --git a/src/core/config/transformer/subgraph.rs b/src/core/config/transformer/subgraph.rs new file mode 100644 index 0000000000..3ef4126ee2 --- /dev/null +++ b/src/core/config/transformer/subgraph.rs @@ -0,0 +1,493 @@ +use std::borrow::Borrow; +use std::collections::BTreeMap; +use std::convert::identity; +use std::fmt::{Display, Write}; +use std::ops::Deref; + +use tailcall_macros::MergeRight; + +use crate::core::config::{ + self, ApolloFederation, Arg, Call, Config, Field, GraphQL, Grpc, Http, Key, KeyValue, Resolver, + Union, +}; +use crate::core::directive::DirectiveCodec; +use crate::core::merge_right::MergeRight; +use crate::core::mustache::Segment; +use crate::core::valid::{Valid, Validator}; +use crate::core::{Mustache, Transform, Type}; + +const ENTITIES_FIELD_NAME: &str = "_entities"; +const SERVICE_FIELD_NAME: &str = "_service"; +const SERVICE_TYPE_NAME: &str = "_Service"; +const UNION_ENTITIES_NAME: &str = "_Entity"; +const ENTITIES_ARG_NAME: &str = "representations"; +const ENTITIES_TYPE_NAME: &str = "_Any"; + +/// Adds compatibility layer for Apollo Federation v2 +/// so tailcall may act as a Federation Subgraph. +/// Followed by [spec](https://www.apollographql.com/docs/federation/subgraph-spec/) +pub struct Subgraph; + +impl Transform for Subgraph { + type Value = Config; + + type Error = String; + + fn transform(&self, mut config: Self::Value) -> Valid { + let mut resolver_by_type = BTreeMap::new(); + + let valid = Valid::from_iter(config.types.iter_mut(), |(type_name, ty)| { + if let Some(resolver) = &ty.resolver { + resolver_by_type.insert(type_name.clone(), resolver.clone()); + + KeysExtractor::extract_keys(resolver).map(|fields| { + fields.map(|fields| { + ty.key = Some(Key { fields }); + }) + }) + } else { + Valid::succeed(None) + } + .trace(type_name) + }); + + if valid.is_fail() { + return valid.map_to(config); + } + + if resolver_by_type.is_empty() { + return Valid::succeed(config); + } + + let entity_union = Union { + types: resolver_by_type.keys().cloned().collect(), + ..Default::default() + }; + + let entity_resolver = config::EntityResolver { resolver_by_type }; + + // union that wraps any possible types for entities + config + .unions + .insert(UNION_ENTITIES_NAME.to_owned(), entity_union); + // any scalar for argument `representations` + config + .types + .insert(ENTITIES_TYPE_NAME.to_owned(), config::Type::default()); + + let service_field = Field { type_of: "String".to_string().into(), ..Default::default() }; + + let service_type = config::Type { + fields: [("sdl".to_string(), service_field)].into_iter().collect(), + ..Default::default() + }; + + // type that represents the response for service + config + .types + .insert(SERVICE_TYPE_NAME.to_owned(), service_type); + + let query_type = match config.schema.query.as_ref() { + Some(name) => name, + None => { + config.schema.query = Some("Query".to_string()); + "Query" + } + }; + + let query_type = config.types.entry(query_type.to_owned()).or_default(); + + let arg = Arg { + type_of: Type::from(ENTITIES_TYPE_NAME.to_string()) + .into_required() + .into_list() + .into_required(), + ..Default::default() + }; + + query_type.fields.insert( + ENTITIES_FIELD_NAME.to_string(), + Field { + type_of: Type::from(UNION_ENTITIES_NAME.to_owned()) + .into_list() + .into_required(), + args: [(ENTITIES_ARG_NAME.to_owned(), arg)].into_iter().collect(), + doc: Some("Apollo federation Query._entities resolver".to_string()), + resolver: Some(Resolver::ApolloFederation( + ApolloFederation::EntityResolver(entity_resolver), + )), + ..Default::default() + }, + ); + + query_type.fields.insert( + SERVICE_FIELD_NAME.to_string(), + Field { + type_of: Type::from(SERVICE_TYPE_NAME.to_owned()).into_required(), + doc: Some("Apollo federation Query._service resolver".to_string()), + resolver: Some(Resolver::ApolloFederation(ApolloFederation::Service)), + ..Default::default() + }, + ); + + Valid::succeed(config) + } +} + +#[derive(Default, Clone, Debug, MergeRight)] +struct Keys(BTreeMap); + +impl Keys { + fn new() -> Self { + Self::default() + } + + fn set_path(&mut self, path: impl Iterator) { + let mut map = &mut self.0; + + for part in path { + map = &mut map.entry(part).or_default().0; + } + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Display for Keys { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (i, (key, value)) in self.0.iter().enumerate() { + f.write_str(key)?; + + if !value.0.is_empty() { + write!(f, " {{ {} }}", value)?; + } + + if i < self.0.len() - 1 { + f.write_char(' ')?; + } + } + + Ok(()) + } +} + +fn combine_keys(v: Vec) -> Keys { + v.into_iter() + .fold(Keys::new(), |acc, keys| acc.merge_right(keys)) +} + +struct KeysExtractor; + +impl KeysExtractor { + fn extract_keys(resolver: &Resolver) -> Valid, String> { + // TODO: add validation for available fields from the type + match resolver { + Resolver::Http(http) => { + Valid::from_iter( + [ + Self::parse_str_option(http.base_url.as_deref()).trace("base_url"), + Self::parse_str(&http.path).trace("path"), + Self::parse_str_option(http.body.as_deref()).trace("body"), + Self::parse_key_value_iter(http.headers.iter()).trace("headers"), + Self::parse_key_value_iter(http.query.iter().map(|q| KeyValue { + key: q.key.to_string(), + value: q.value.to_string(), + })) + .trace("query"), + ], + identity, + ) + .trace(Http::directive_name().as_str()) + } + Resolver::Grpc(grpc) => Valid::from_iter( + [ + Self::parse_str_option(grpc.base_url.as_deref()), + Self::parse_str(&grpc.method), + Self::parse_value_option(&grpc.body), + Self::parse_key_value_iter(grpc.headers.iter()), + ], + identity, + ) + .trace(Grpc::directive_name().as_str()), + Resolver::Graphql(graphql) => Valid::from_iter( + [ + Self::parse_key_value_iter(graphql.headers.iter()), + Self::parse_key_value_iter_option(graphql.args.as_ref().map(|v| v.iter())), + ], + identity, + ) + .trace(GraphQL::directive_name().as_str()), + Resolver::Call(call) => Valid::from_option( + call.steps.first(), + "Call should define at least one step".to_string(), + ) + .and_then(|step| { + Valid::from_iter(step.args.iter(), |(key, value)| { + Valid::from_iter([Self::parse_str(key), Self::parse_value(value)], identity) + }) + }) + .map(|v| v.into_iter().flatten().collect()) + .trace(Call::directive_name().as_str()), + Resolver::Expr(expr) => Valid::from_iter([Self::parse_value(&expr.body)], identity) + .trace(Call::directive_name().as_str()), + _ => return Valid::succeed(None), + } + .map(|keys| { + let keys = combine_keys(keys); + + if keys.is_empty() { + None + } else { + Some(keys.to_string()) + } + }) + } + + fn parse_str(s: &str) -> Valid { + let mustache = Mustache::parse(s); + let mut keys = Keys::new(); + + Valid::from_iter(mustache.segments().iter(), |segment| { + if let Segment::Expression(expr) = segment { + match expr.first().map(Deref::deref) { + Some("value") => { + keys.set_path(expr[1..].iter().map(String::to_string)); + } + Some("args") => { + return Valid::fail( + "Type resolver can't use `.args`, use `.value` instead".to_string(), + ); + } + _ => {} + } + } + + Valid::succeed(()) + }) + .map_to(keys) + } + + fn parse_str_option(s: Option<&str>) -> Valid { + if let Some(s) = s { + Self::parse_str(s) + } else { + Valid::succeed(Keys::new()) + } + } + + fn parse_key_value_iter>( + it: impl Iterator, + ) -> Valid { + let mut keys = Keys::new(); + + Valid::from_iter(it, |key_value| { + let key_value = key_value.borrow(); + + Self::parse_str(&key_value.key) + .zip(Self::parse_str(&key_value.value)) + .map(|(key, value)| keys = keys.clone().merge_right(key).merge_right(value)) + }) + .map_to(keys) + } + + fn parse_key_value_iter_option>( + it: Option>, + ) -> Valid { + if let Some(it) = it { + Self::parse_key_value_iter(it) + } else { + Valid::succeed(Keys::new()) + } + } + + fn parse_value(value: &serde_json::Value) -> Valid { + match value { + serde_json::Value::String(s) => return Self::parse_str(s), + serde_json::Value::Array(v) => Valid::from_iter(v.iter(), Self::parse_value), + serde_json::Value::Object(map) => Valid::from_iter(map.iter(), |(k, v)| { + Self::parse_str(k) + .zip(Self::parse_value(v)) + .map(|(k, v)| k.merge_right(v)) + }), + _ => return Valid::succeed(Keys::new()), + } + .map(|keys_vec| { + keys_vec + .into_iter() + .fold(Keys::new(), |acc, keys| acc.merge_right(keys)) + }) + } + + fn parse_value_option(value: &Option) -> Valid { + if let Some(value) = value { + Self::parse_value(value) + } else { + Valid::succeed(Keys::new()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + mod keys { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn test_keys_set() { + let mut keys = Keys::new(); + + keys.set_path(["a", "b", "c"].into_iter().map(str::to_string)); + keys.set_path(["a", "d"].into_iter().map(str::to_string)); + keys.set_path(["e"].into_iter().map(str::to_string)); + keys.set_path(["f", "g"].into_iter().map(str::to_string)); + + assert_debug_snapshot!(keys); + } + + #[test] + fn test_keys_merge() { + let mut keys_a = Keys::new(); + + keys_a.set_path(["a", "b"].into_iter().map(str::to_string)); + keys_a.set_path(["c"].into_iter().map(str::to_string)); + + let mut keys_b = Keys::new(); + + keys_b.set_path(["a", "1"].into_iter().map(str::to_string)); + keys_b.set_path(["c", "2"].into_iter().map(str::to_string)); + keys_b.set_path(["d", "3"].into_iter().map(str::to_string)); + + assert_debug_snapshot!(keys_a.merge_right(keys_b)); + } + } + + mod extractor { + use insta::assert_debug_snapshot; + use serde_json::json; + + use super::config::Http; + use super::{KeyValue, KeysExtractor, Resolver}; + use crate::core::config::{Call, Expr, GraphQL, Grpc, Step, URLQuery}; + use crate::core::http::Method; + + #[test] + fn test_non_value_template() { + let http = Http { + base_url: Some("http://tailcall.run".to_string()), + path: "users/{{.args.id}}".to_string(), + query: vec![URLQuery { + key: "{{.env.query.key}}".to_string(), + value: "{{.args.query.value}}".to_string(), + ..Default::default() + }], + ..Default::default() + }; + let resolver = Resolver::Http(http); + let keys = KeysExtractor::extract_keys(&resolver); + + assert_debug_snapshot!(keys); + } + + #[test] + fn test_extract_http() { + let http = Http { + base_url: Some("http://tailcall.run".to_string()), + body: Some(r#"{ "obj": "{{.value.obj}}"} "#.to_string()), + headers: vec![KeyValue { + key: "{{.value.header.key}}".to_string(), + value: "{{.value.header.value}}".to_string(), + }], + method: Method::POST, + path: "users/{{.value.id}}".to_string(), + query: vec![URLQuery { + key: "{{.value.query_key}}".to_string(), + value: "{{.value.query_value}}".to_string(), + ..Default::default() + }], + ..Default::default() + }; + let resolver = Resolver::Http(http); + let keys = KeysExtractor::extract_keys(&resolver); + + assert_debug_snapshot!(keys); + } + + #[test] + fn test_extract_grpc() { + let grpc = Grpc { + base_url: Some("http://localhost:5051/{{.env.target}}".to_string()), + body: Some(json!({ "a": "{{.value.body.a}}", "b": "{{.value.body.b}}"})), + headers: vec![KeyValue { + key: "test".to_string(), + value: "{{.value.header_test}}".to_string(), + }], + method: "test_{{.value.method}}".to_string(), + ..Default::default() + }; + + let resolver = Resolver::Grpc(grpc); + let keys = KeysExtractor::extract_keys(&resolver); + + assert_debug_snapshot!(keys); + } + + #[test] + fn test_extract_graphql() { + let graphql = GraphQL { + base_url: Some("http://localhost:5051/{{.env.target}}".to_string()), + headers: vec![KeyValue { + key: "test".to_string(), + value: "{{.value.header_test}}".to_string(), + }], + args: Some(vec![KeyValue { + key: "key".to_string(), + value: "test-{{.value.input.key}}".to_string(), + }]), + ..Default::default() + }; + + let resolver = Resolver::Graphql(graphql); + let keys = KeysExtractor::extract_keys(&resolver); + + assert_debug_snapshot!(keys); + } + + #[test] + fn test_extract_call() { + let call = Call { + steps: vec![Step { + query: Some("field".to_string()), + args: [( + "arg".to_string(), + json!(json!({ "a": "{{.value.arg.a}}", "b": "{{.value.arg.b}}"})), + )] + .into_iter() + .collect(), + ..Default::default() + }], + }; + + let resolver = Resolver::Call(call); + let keys = KeysExtractor::extract_keys(&resolver); + + assert_debug_snapshot!(keys); + } + + #[test] + fn test_extract_expr() { + let expr = Expr { + body: json!({ "a": "{{.value.body.a}}", "b": "{{.value.body.b}}"}), + }; + + let resolver = Resolver::Expr(expr); + let keys = KeysExtractor::extract_keys(&resolver); + + assert_debug_snapshot!(keys); + } + } +} diff --git a/src/core/helpers/body.rs b/src/core/helpers/body.rs index aa498644d5..0f2b582577 100644 --- a/src/core/helpers/body.rs +++ b/src/core/helpers/body.rs @@ -13,6 +13,7 @@ pub fn to_body(body: Option<&Value>) -> Valid, String> { let value = body.to_string(); let mustache = Mustache::parse(&value); + // TODO: req_body.mustache is always set making req_body.value useless req_body = req_body.mustache(Some(mustache)); Valid::succeed(Some(req_body.value(value))) diff --git a/src/core/ir/error.rs b/src/core/ir/error.rs index 2bcd513f83..7f026f83a7 100644 --- a/src/core/ir/error.rs +++ b/src/core/ir/error.rs @@ -6,6 +6,7 @@ use derive_more::From; use thiserror::Error; use crate::core::{auth, cache, worker, Errata}; + #[derive(From, Debug, Error, Clone)] pub enum Error { IO(String), @@ -30,6 +31,9 @@ pub enum Error { Worker(worker::Error), Cache(cache::Error), + + #[from(ignore)] + Entity(String), } impl Display for Error { @@ -62,6 +66,7 @@ impl From for Errata { } Error::Worker(err) => Errata::new("Worker Error").description(err.to_string()), Error::Cache(err) => Errata::new("Cache Error").description(err.to_string()), + Error::Entity(message) => Errata::new("Entity Resolver Error").description(message) } } } diff --git a/src/core/ir/eval.rs b/src/core/ir/eval.rs index 6c5cc40433..234197b751 100644 --- a/src/core/ir/eval.rs +++ b/src/core/ir/eval.rs @@ -2,11 +2,13 @@ use std::future::Future; use std::ops::Deref; use async_graphql_value::ConstValue; +use futures_util::future::join_all; +use indexmap::IndexMap; use super::eval_io::eval_io; use super::model::{Cache, CacheKey, Map, IR}; use super::{Error, EvalContext, ResolverContextLike, TypedValue}; -use crate::core::json::{JsonLike, JsonLikeList}; +use crate::core::json::{JsonLike, JsonLikeList, JsonObjectLike}; use crate::core::serde_value_ext::ValueExt; // Fake trait to capture proper lifetimes. @@ -87,6 +89,12 @@ impl IR { } IR::Discriminate(discriminator, expr) => expr.eval(ctx).await.and_then(|value| { let value = value.map(&mut |mut value| { + if value.get_type_name().is_some() { + // if typename is already present in value just reuse it instead + // of recalculating from scratch + return Ok(value); + } + let type_name = discriminator.resolve_type(&value)?; value.set_type_name(type_name.to_string())?; @@ -96,6 +104,54 @@ impl IR { Ok(value) }), + IR::Entity(map) => { + let representations = ctx.path_arg(&["representations"]); + + let representations = representations + .as_ref() + .and_then(|repr| repr.as_array()) + .ok_or(Error::Entity( + "expected `representations` arg as an array of _Any".to_string(), + ))?; + + let mut tasks = Vec::with_capacity(representations.len()); + + for repr in representations { + // TODO: combine errors, instead of fail fast? + let type_name = repr.get_type_name().ok_or(Error::Entity( + "expected __typename to be the part of the representation".to_string(), + ))?; + + let ir = map.get(type_name).ok_or(Error::Entity(format!( + "Cannot find a resolver for type: `{type_name}`" + )))?; + + // pass the input for current representation as value in context + // TODO: can we drop clone? + let mut ctx = ctx.with_value(repr.clone()); + + tasks.push(async move { + ir.eval(&mut ctx).await.and_then(|mut value| { + // set typename explicitly to reuse it if needed + value.set_type_name(type_name.to_owned())?; + Ok(value) + }) + }); + } + + let result = join_all(tasks).await; + + let entities = result.into_iter().collect::>()?; + + Ok(ConstValue::List(entities)) + } + IR::Service(sdl) => { + let mut obj = IndexMap::new(); + + obj.insert_key("sdl", ConstValue::string(sdl.into())); + + Ok(ConstValue::object(obj)) + } } }) } diff --git a/src/core/ir/model.rs b/src/core/ir/model.rs index 89d38ee317..3473d47d96 100644 --- a/src/core/ir/model.rs +++ b/src/core/ir/model.rs @@ -26,6 +26,10 @@ pub enum IR { Map(Map), Pipe(Box, Box), Discriminate(Discriminator, Box), + /// Apollo Federation _entities resolver + Entity(HashMap), + /// Apollo Federation _service resolver + Service(String), } #[derive(Clone, Debug)] @@ -42,6 +46,7 @@ pub enum IO { group_by: Option, dl_id: Option, http_filter: Option, + is_list: bool, }, GraphQL { req_template: graphql::RequestTemplate, @@ -101,7 +106,7 @@ impl Cache { /// Performance DFS on the cache on the expression and identifies all the IO /// nodes. Then wraps each IO node with the cache primitive. pub fn wrap(max_age: NonZeroU64, expr: IR) -> IR { - expr.modify(move |expr| match expr { + expr.modify(&mut move |expr| match expr { IR::IO(io) => Some(IR::Cache(Cache { max_age, io: Box::new(io.to_owned()) })), _ => None, }) @@ -113,8 +118,8 @@ impl IR { IR::Pipe(Box::new(self), Box::new(next)) } - pub fn modify(self, mut f: impl FnMut(&IR) -> Option) -> IR { - self.modify_inner(&mut f) + pub fn modify Option>(self, modifier: &mut F) -> IR { + self.modify_inner(modifier) } fn modify_box Option>(self, modifier: &mut F) -> Box { @@ -149,6 +154,12 @@ impl IR { IR::Discriminate(discriminator, expr) => { IR::Discriminate(discriminator, expr.modify_box(modifier)) } + IR::Entity(map) => IR::Entity( + map.into_iter() + .map(|(k, v)| (k, v.modify(modifier))) + .collect(), + ), + IR::Service(sdl) => IR::Service(sdl), } } } diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index 099bb47f5a..a814e22f53 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -9,7 +9,6 @@ use nom::{Finish, IResult}; use super::*; impl Mustache { - // TODO: infallible function, no need to return Result pub fn parse(str: &str) -> Mustache { let result = parse_mustache(str).finish(); match result { diff --git a/src/core/valid/valid.rs b/src/core/valid/valid.rs index 173897cf85..cda47c82db 100644 --- a/src/core/valid/valid.rs +++ b/src/core/valid/valid.rs @@ -25,6 +25,8 @@ pub trait Validator: Sized { fn is_succeed(&self) -> bool; + fn is_fail(&self) -> bool; + fn and(self, other: Valid) -> Valid { self.zip(other).map(|(_, a1)| a1) } @@ -165,6 +167,10 @@ impl Validator for Valid { fn is_succeed(&self) -> bool { self.0.is_ok() } + + fn is_fail(&self) -> bool { + self.0.is_err() + } } pub struct Fusion(Valid); @@ -184,6 +190,9 @@ impl Validator for Fusion { fn is_succeed(&self) -> bool { self.0.is_succeed() } + fn is_fail(&self) -> bool { + self.0.is_fail() + } } impl From>> for Valid { diff --git a/tailcall-macros/src/lib.rs b/tailcall-macros/src/lib.rs index 377a4ef20e..5508b6235f 100644 --- a/tailcall-macros/src/lib.rs +++ b/tailcall-macros/src/lib.rs @@ -1,6 +1,7 @@ extern crate proc_macro; use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; mod document_definition; mod gen; @@ -48,7 +49,10 @@ pub fn input_definition_derive(input: TokenStream) -> TokenStream { expand_input_definition(input) } -#[proc_macro_derive(CustomResolver)] +#[proc_macro_derive(CustomResolver, attributes(resolver))] pub fn resolver_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); expand_resolver_derive(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() } diff --git a/tailcall-macros/src/merge_right.rs b/tailcall-macros/src/merge_right.rs index dce5c428f5..03c8556ca0 100644 --- a/tailcall-macros/src/merge_right.rs +++ b/tailcall-macros/src/merge_right.rs @@ -3,7 +3,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn::spanned::Spanned; -use syn::{parse_macro_input, Data, DeriveInput, Fields}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Index}; const MERGE_RIGHT_FN: &str = "merge_right_fn"; const MERGE_RIGHT: &str = "merge_right"; @@ -59,29 +59,54 @@ pub fn expand_merge_right_derive(input: TokenStream) -> TokenStream { let gen = match input.data { // Implement for structs Data::Struct(data) => { - let fields = if let Fields::Named(fields) = data.fields { - fields.named - } else { - // Adjust this match arm to handle other kinds of struct fields (unnamed/tuple - // structs, unit structs) - unimplemented!() + let fields = match &data.fields { + Fields::Named(fields) => &fields.named, + Fields::Unnamed(fields) => &fields.unnamed, + Fields::Unit => { + return quote! { + impl MergeRight for #name { + fn merge_right(self, other: Self) -> Self { + other + } + } + } + .into() + } }; - let merge_logic = fields.iter().map(|f| { + let merge_logic = fields.iter().enumerate().map(|(i, f)| { let attrs = get_attrs(&f.attrs); if let Err(err) = attrs { panic!("{}", err); } let attrs = attrs.unwrap(); let name = &f.ident; - if let Some(merge_right_fn) = attrs.merge_right_fn { - quote! { - #name: #merge_right_fn(self.#name, other.#name), + + match &data.fields { + Fields::Named(_) => { + if let Some(merge_right_fn) = attrs.merge_right_fn { + quote! { + #name: #merge_right_fn(self.#name, other.#name), + } + } else { + quote! { + #name: self.#name.merge_right(other.#name), + } + } } - } else { - quote! { - #name: self.#name.merge_right(other.#name), + Fields::Unnamed(_) => { + let name = Index::from(i); + if let Some(merge_right_fn) = attrs.merge_right_fn { + quote! { + #merge_right_fn(self.#name, other.#name), + } + } else { + quote! { + self.#name.merge_right(other.#name), + } + } } + Fields::Unit => unreachable!(), } }); @@ -93,12 +118,22 @@ pub fn expand_merge_right_derive(input: TokenStream) -> TokenStream { #generics_lt #generics_params #generics_gt }; + let initializer = match data.fields { + Fields::Named(_) => quote! { + Self { + #(#merge_logic)* + } + }, + Fields::Unnamed(_) => quote! { + Self(#(#merge_logic)*) + }, + Fields::Unit => unreachable!(), + }; + quote! { impl #generics_del MergeRight for #name #generics_del { fn merge_right(self, other: Self) -> Self { - Self { - #(#merge_logic)* - } + #initializer } } } diff --git a/tailcall-macros/src/resolver.rs b/tailcall-macros/src/resolver.rs index cb6526af1e..99cd446c4f 100644 --- a/tailcall-macros/src/resolver.rs +++ b/tailcall-macros/src/resolver.rs @@ -1,9 +1,36 @@ -use proc_macro::TokenStream; +use proc_macro2::TokenStream; use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, Fields}; +use syn::{Attribute, Data, DeriveInput, Fields}; -pub fn expand_resolver_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); +const ATTR_NAMESPACE: &str = "resolver"; +const ATTR_SKIP_DIRECTIVE: &str = "skip_directive"; + +#[derive(Default)] +struct Attrs { + skip_directive: bool, +} + +fn parse_attrs(attributes: &Vec) -> syn::Result { + let mut result = Attrs::default(); + + for attr in attributes { + if attr.path().is_ident(ATTR_NAMESPACE) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident(ATTR_SKIP_DIRECTIVE) { + result.skip_directive = true; + + return Ok(()); + } + + Err(meta.error("unrecognized resolver attribute")) + })?; + } + } + + Ok(result) +} + +pub fn expand_resolver_derive(input: DeriveInput) -> syn::Result { let name = &input.ident; let variants = if let Data::Enum(data_enum) = &input.data { @@ -12,20 +39,27 @@ pub fn expand_resolver_derive(input: TokenStream) -> TokenStream { .iter() .map(|variant| { let variant_name = &variant.ident; + let attrs = &variant.attrs; let ty = match &variant.fields { Fields::Unnamed(fields) if fields.unnamed.len() == 1 => &fields.unnamed[0].ty, _ => panic!("Resolver variants must have exactly one unnamed field"), }; - (variant_name, ty) + let attrs = parse_attrs(attrs)?; + + Ok((variant_name, ty, attrs)) }) - .collect::>() + .collect::>>()? } else { panic!("Resolver can only be derived for enums"); }; - let variant_parsers = variants.iter().map(|(variant_name, ty)| { - quote! { + let variant_parsers = variants.iter().filter_map(|(variant_name, ty, attrs)| { + if attrs.skip_directive { + return None; + } + + Some(quote! { valid = valid.and(<#ty>::from_directives(directives.iter()).map(|resolver| { if let Some(resolver) = resolver { let directive_name = <#ty>::trace_name(); @@ -35,18 +69,30 @@ pub fn expand_resolver_derive(input: TokenStream) -> TokenStream { result = Some(Self::#variant_name(resolver)); } })); - } + }) }); - let match_arms_to_directive = variants.iter().map(|(variant_name, _ty)| { - quote! { - Self::#variant_name(d) => d.to_directive(), + let match_arms_to_directive = variants.iter().map(|(variant_name, _ty, attrs)| { + if attrs.skip_directive { + quote! { + Self::#variant_name(d) => None, + } + } else { + quote! { + Self::#variant_name(d) => Some(d.to_directive()), + } } }); - let match_arms_directive_name = variants.iter().map(|(variant_name, ty)| { - quote! { - Self::#variant_name(_) => <#ty>::directive_name(), + let match_arms_directive_name = variants.iter().map(|(variant_name, ty, attrs)| { + if attrs.skip_directive { + quote! { + Self::#variant_name(_) => String::new(), + } + } else { + quote! { + Self::#variant_name(_) => <#ty>::directive_name(), + } } }); @@ -73,7 +119,7 @@ pub fn expand_resolver_derive(input: TokenStream) -> TokenStream { }) } - pub fn to_directive(&self) -> ConstDirective { + pub fn to_directive(&self) -> Option { match self { #(#match_arms_to_directive)* } @@ -87,5 +133,5 @@ pub fn expand_resolver_derive(input: TokenStream) -> TokenStream { } }; - TokenStream::from(expanded) + Ok(expanded) } diff --git a/tests/core/snapshots/apollo-federation-entities-batch.md_0.snap b/tests/core/snapshots/apollo-federation-entities-batch.md_0.snap new file mode 100644 index 0000000000..cdd18ea9a9 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities-batch.md_0.snap @@ -0,0 +1,26 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "_entities": [ + { + "__typename": "User", + "id": 1, + "name": "Leanne Graham" + }, + { + "__typename": "User", + "id": 2, + "name": "Ervin Howell" + } + ] + } + } +} diff --git a/tests/core/snapshots/apollo-federation-entities-batch.md_1.snap b/tests/core/snapshots/apollo-federation-entities-batch.md_1.snap new file mode 100644 index 0000000000..311df64dd7 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities-batch.md_1.snap @@ -0,0 +1,17 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "_service": { + "sdl": "schema @server(port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @graphQL(args: [{key: \"id\", value: \"{{.value.id}}\"}], baseURL: \"http://upstream/graphql\", batch: true, name: \"post\") @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @http(batchKey: [\"id\"], path: \"/users\", query: [{key: \"id\", value: \"{{.value.user.id}}\"}]) @key(fields: \"user { id }\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\n\"\"\"\nThe @addField operator simplifies data structures and queries by adding a field that \ninlines or flattens a nested field or node within your schema. more info [here](https://tailcall.run/docs/guides/operators/#addfield)\n\"\"\"\ndirective @addField(\n \"\"\"\n Name of the new field to be added\n \"\"\"\n name: String!\n \"\"\"\n Path of the data where the field should point to\n \"\"\"\n path: [String!]\n) repeatable on OBJECT\n\n\"\"\"\nThe @alias directive indicates that aliases of one enum value.\n\"\"\"\ndirective @alias(\n options: [String!]\n) on ENUM_VALUE\n\n\"\"\"\nThe @cache operator enables caching for the query, field or type it is applied to.\n\"\"\"\ndirective @cache(\n \"\"\"\n Specifies the duration, in milliseconds, of how long the value has to be stored in \n the cache.\n \"\"\"\n maxAge: Int!\n) on OBJECT | FIELD_DEFINITION\n\n\"\"\"\nProvides the ability to refer to multiple fields in the Query or Mutation root.\n\"\"\"\ndirective @call(\n \"\"\"\n Steps are composed together to form a call. If you have multiple steps, the output \n of the previous step is passed as input to the next step.\n \"\"\"\n steps: [Step]\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe `@expr` operators allows you to specify an expression that can evaluate to a \nvalue. The expression can be a static value or built form a Mustache template. schema.\n\"\"\"\ndirective @expr(\n body: JSON\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe @graphQL operator allows to specify GraphQL API server request to fetch data \nfrom.\n\"\"\"\ndirective @graphQL(\n \"\"\"\n Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args)\n \"\"\"\n args: [KeyValue]\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n If the upstream GraphQL server supports request batching, you can specify the 'batch' \n argument to batch several requests into a single batch request.Make sure you have \n also specified batch settings to the `@upstream` and to the `@graphQL` operator.\n \"\"\"\n batch: Boolean!\n \"\"\"\n The headers parameter allows you to customize the headers of the GraphQL request \n made by the `@graphQL` operator. It is used by specifying a key-value map of header \n names and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Specifies the root field on the upstream to request data from. This maps a field \n in your schema to a field in the upstream schema. When a query is received for this \n field, Tailcall requests data from the corresponding upstream field.\n \"\"\"\n name: String!\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe @grpc operator indicates that a field or node is backed by a gRPC API.For instance, \nif you add the @grpc operator to the `users` field of the Query type with a service \nargument of `NewsService` and method argument of `GetAllNews`, it signifies that \nthe `users` field is backed by a gRPC API. The `service` argument specifies the name \nof the gRPC service. The `method` argument specifies the name of the gRPC method. \nIn this scenario, the GraphQL server will make a gRPC request to the gRPC endpoint \nspecified when the `users` field is queried.\n\"\"\"\ndirective @grpc(\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n This refers to the arguments of your gRPC call. You can pass it as a static object \n or use Mustache template for dynamic parameters. These parameters will be added in \n the body in `protobuf` format.\n \"\"\"\n body: JSON\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@grpc` operator. It is used by specifying a key-value map of header names \n and their values. Note: content-type is automatically set to application/grpc\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n This refers to the gRPC method you're going to call. For instance `GetAllNews`.\n \"\"\"\n method: String!\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe @http operator indicates that a field or node is backed by a REST API.For instance, \nif you add the @http operator to the `users` field of the Query type with a path \nargument of `\"/users\"`, it signifies that the `users` field is backed by a REST API. \nThe path argument specifies the path of the REST API. In this scenario, the GraphQL \nserver will make a GET request to the API endpoint specified when the `users` field \nis queried.\n\"\"\"\ndirective @http(\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n The body of the API call. It's used for methods like POST or PUT that send data to \n the server. You can pass it as a static object or use a Mustache template to substitute \n variables from the GraphQL variables.\n \"\"\"\n body: String\n \"\"\"\n The `encoding` parameter specifies the encoding of the request body. It can be `ApplicationJson` \n or `ApplicationXWwwFormUrlEncoded`. @default `ApplicationJson`.\n \"\"\"\n encoding: Encoding\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@http` operator. It is used by specifying a key-value map of header names \n and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Schema of the input of the API call. It is automatically inferred in most cases.\n \"\"\"\n input: Schema\n \"\"\"\n This refers to the HTTP method of the API call. Commonly used methods include `GET`, \n `POST`, `PUT`, `DELETE` etc. @default `GET`.\n \"\"\"\n method: Method\n \"\"\"\n onRequest field in @http directive gives the ability to specify the request interception \n handler.\n \"\"\"\n onRequest: String\n \"\"\"\n Schema of the output of the API call. It is automatically inferred in most cases.\n \"\"\"\n output: Schema\n \"\"\"\n This refers to the API endpoint you're going to call. For instance `https://jsonplaceholder.typicode.com/users`.For \n dynamic segments in your API endpoint, use Mustache templates for variable substitution. \n For instance, to fetch a specific user, use `/users/{{args.id}}`.\n \"\"\"\n path: String!\n \"\"\"\n This represents the query parameters of your API call. You can pass it as a static \n object or use Mustache template for dynamic parameters. These parameters will be \n added to the URL. NOTE: Query parameter order is critical for batching in Tailcall. \n The first parameter referencing a field in the current value using mustache syntax \n is automatically selected as the batching parameter.\n \"\"\"\n query: [URLQuery]\n) on FIELD_DEFINITION | OBJECT\n\ndirective @js(\n name: String!\n) on FIELD_DEFINITION | OBJECT\n\ndirective @modify(\n name: String\n omit: Boolean\n) on FIELD_DEFINITION\n\n\"\"\"\nUsed to omit a field from public consumption.\n\"\"\"\ndirective @omit on FIELD_DEFINITION\n\ndirective @protected on OBJECT | FIELD_DEFINITION\n\n\"\"\"\nThe `@server` directive, when applied at the schema level, offers a comprehensive \nset of server configurations. It dictates how the server behaves and helps tune tailcall \nfor various use-cases.\n\"\"\"\ndirective @server(\n \"\"\"\n `apolloTracing` exposes GraphQL query performance data, including execution time \n of queries and individual resolvers.\n \"\"\"\n apolloTracing: Boolean\n \"\"\"\n `batchRequests` combines multiple requests into one, improving performance but potentially \n introducing latency and complicating debugging. Use judiciously. @default `false`.\n \"\"\"\n batchRequests: Boolean\n \"\"\"\n Enables deduplication of IO operations to enhance performance.This flag prevents \n duplicate IO requests from being executed concurrently, reducing resource load. Caution: \n May lead to issues with APIs that expect unique results for identical inputs, such \n as nonce-based APIs.\n \"\"\"\n dedupe: Boolean\n enableJIT: Boolean\n \"\"\"\n `globalResponseTimeout` sets the maximum query duration before termination, acting \n as a safeguard against long-running queries.\n \"\"\"\n globalResponseTimeout: Int\n \"\"\"\n `headers` contains key-value pairs that are included as default headers in server \n responses, allowing for consistent header management across all responses.\n \"\"\"\n headers: Headers\n \"\"\"\n `hostname` sets the server hostname.\n \"\"\"\n hostname: String\n \"\"\"\n `introspection` allows clients to fetch schema information directly, aiding tools \n and applications in understanding available types, fields, and operations. @default \n `true`.\n \"\"\"\n introspection: Boolean\n \"\"\"\n `pipelineFlush` allows to control flushing behavior of the server pipeline.\n \"\"\"\n pipelineFlush: Boolean\n \"\"\"\n `port` sets the Tailcall running port. @default `8000`.\n \"\"\"\n port: Int\n \"\"\"\n `queryValidation` checks incoming GraphQL queries against the schema, preventing \n errors from invalid queries. Can be disabled for performance. @default `false`.\n \"\"\"\n queryValidation: Boolean\n \"\"\"\n `responseValidation` Tailcall automatically validates responses from upstream services \n using inferred schema. @default `false`.\n \"\"\"\n responseValidation: Boolean\n \"\"\"\n `routes` allows customization of server endpoint paths. It provides options to change \n the default paths for status and GraphQL endpoints. Default values are: - status: \n \"/status\" - graphQL: \"/graphql\" If not specified, these default values will be used.\n \"\"\"\n routes: Routes\n \"\"\"\n A link to an external JS file that listens on every HTTP request response event.\n \"\"\"\n script: ScriptOptions\n \"\"\"\n `showcase` enables the /showcase/graphql endpoint.\n \"\"\"\n showcase: Boolean\n \"\"\"\n This configuration defines local variables for server operations. Useful for storing \n constant configurations, secrets, or shared information.\n \"\"\"\n vars: [KeyValue]\n \"\"\"\n `version` sets the HTTP version for the server. Options are `HTTP1` and `HTTP2`. \n @default `HTTP1`.\n \"\"\"\n version: HttpVersion\n \"\"\"\n `workers` sets the number of worker threads. @default the number of system cores.\n \"\"\"\n workers: Int\n) on SCHEMA\n\n\"\"\"\nThe @telemetry directive facilitates seamless integration with OpenTelemetry, enhancing \nthe observability of your GraphQL services powered by Tailcall. By leveraging this \ndirective, developers gain access to valuable insights into the performance and behavior \nof their applications.\n\"\"\"\ndirective @telemetry(\n export: TelemetryExporter\n \"\"\"\n The list of headers that will be sent as additional attributes to telemetry exporters \n Be careful about **leaking sensitive information** from requests when enabling the \n headers that may contain sensitive data\n \"\"\"\n requestHeaders: [String!]\n) on SCHEMA\n\n\"\"\"\nThe `upstream` directive allows you to control various aspects of the upstream server \nconnection. This includes settings like connection timeouts, keep-alive intervals, \nand more. If not specified, default values are used.\n\"\"\"\ndirective @upstream(\n \"\"\"\n `allowedHeaders` defines the HTTP headers allowed to be forwarded to upstream services. \n If not set, no headers are forwarded, enhancing security but possibly limiting data \n flow.\n \"\"\"\n allowedHeaders: [String!]\n \"\"\"\n This refers to the default base URL for your APIs. If it's not explicitly mentioned \n in the `@upstream` operator, then each [@http](#http) operator must specify its own \n `baseURL`. If neither `@upstream` nor [@http](#http) provides a `baseURL`, it results \n in a compilation error.\n \"\"\"\n baseURL: String\n \"\"\"\n An object that specifies the batch settings, including `maxSize` (the maximum size \n of the batch), `delay` (the delay in milliseconds between each batch), and `headers` \n (an array of HTTP headers to be included in the batch).\n \"\"\"\n batch: Batch\n \"\"\"\n The time in seconds that the connection will wait for a response before timing out.\n \"\"\"\n connectTimeout: Int\n \"\"\"\n The `http2Only` setting allows you to specify whether the client should always issue \n HTTP2 requests, without checking if the server supports it or not. By default it \n is set to `false` for all HTTP requests made by the server, but is automatically \n set to true for GRPC.\n \"\"\"\n http2Only: Boolean\n \"\"\"\n Providing httpCache size enables Tailcall's HTTP caching, adhering to the [HTTP Caching \n RFC](https://tools.ietf.org/html/rfc7234), to enhance performance by minimizing redundant \n data fetches. Defaults to `0` if unspecified.\n \"\"\"\n httpCache: Int\n \"\"\"\n The time in seconds between each keep-alive message sent to maintain the connection.\n \"\"\"\n keepAliveInterval: Int\n \"\"\"\n The time in seconds that the connection will wait for a keep-alive message before \n closing.\n \"\"\"\n keepAliveTimeout: Int\n \"\"\"\n A boolean value that determines whether keep-alive messages should be sent while \n the connection is idle.\n \"\"\"\n keepAliveWhileIdle: Boolean\n \"\"\"\n onRequest field gives the ability to specify the global request interception handler.\n \"\"\"\n onRequest: String\n \"\"\"\n The time in seconds that the connection pool will wait before closing idle connections.\n \"\"\"\n poolIdleTimeout: Int\n \"\"\"\n The maximum number of idle connections that will be maintained per host.\n \"\"\"\n poolMaxIdlePerHost: Int\n \"\"\"\n The `proxy` setting defines an intermediary server through which the upstream requests \n will be routed before reaching their intended endpoint. By specifying a proxy URL, \n you introduce an additional layer, enabling custom routing and security policies.\n \"\"\"\n proxy: Proxy\n \"\"\"\n The time in seconds between each TCP keep-alive message sent to maintain the connection.\n \"\"\"\n tcpKeepAlive: Int\n \"\"\"\n The maximum time in seconds that the connection will wait for a response.\n \"\"\"\n timeout: Int\n \"\"\"\n The User-Agent header value to be used in HTTP requests. @default `Tailcall/1.0`\n \"\"\"\n userAgent: String\n) on SCHEMA\n\n\"\"\"\nField whose value is a sequence of bytes.\n\"\"\"\nscalar Bytes\n\n\"\"\"\nField whose value conforms to the standard date format as specified in RFC 3339 (https://datatracker.ietf.org/doc/html/rfc3339).\n\"\"\"\nscalar Date\n\n\"\"\"\nField whose value conforms to the standard internet email address format as specified \nin HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address.\n\"\"\"\nscalar Email\n\n\"\"\"\nEmpty scalar type represents an empty value.\n\"\"\"\nscalar Empty\n\n\"\"\"\nField whose value is a 128-bit signed integer.\n\"\"\"\nscalar Int128\n\n\"\"\"\nField whose value is a 16-bit signed integer.\n\"\"\"\nscalar Int16\n\n\"\"\"\nField whose value is a 32-bit signed integer.\n\"\"\"\nscalar Int32\n\n\"\"\"\nField whose value is a 64-bit signed integer.\n\"\"\"\nscalar Int64\n\n\"\"\"\nField whose value is an 8-bit signed integer.\n\"\"\"\nscalar Int8\n\n\"\"\"\nField whose value conforms to the standard JSON format as specified in RFC 8259 (https://datatracker.ietf.org/doc/html/rfc8259).\n\"\"\"\nscalar JSON\n\n\"\"\"\nField whose value conforms to the standard E.164 format as specified in E.164 specification \n(https://en.wikipedia.org/wiki/E.164).\n\"\"\"\nscalar PhoneNumber\n\n\"\"\"\nField whose value is a 128-bit unsigned integer.\n\"\"\"\nscalar UInt128\n\n\"\"\"\nField whose value is a 16-bit unsigned integer.\n\"\"\"\nscalar UInt16\n\n\"\"\"\nField whose value is a 32-bit unsigned integer.\n\"\"\"\nscalar UInt32\n\n\"\"\"\nField whose value is a 64-bit unsigned integer.\n\"\"\"\nscalar UInt64\n\n\"\"\"\nField whose value is an 8-bit unsigned integer.\n\"\"\"\nscalar UInt8\n\n\"\"\"\nField whose value conforms to the standard URL format as specified in RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986).\n\"\"\"\nscalar Url\n\n\"\"\"\nProvides the ability to refer to a field defined in the root Query or Mutation.\n\"\"\"\ninput Step {\n \"\"\"\n The arguments that will override the actual arguments of the field.\n \"\"\"\n args: JSON\n \"\"\"\n The name of the field on the `Mutation` type that you want to call.\n \"\"\"\n mutation: String\n \"\"\"\n The name of the field on the `Query` type that you want to call.\n \"\"\"\n query: String\n}\n\ninput KeyValue {\n key: String!\n value: String!\n}\n\n\"\"\"\nThe URLQuery input type represents a query parameter to be included in a URL.\n\"\"\"\ninput URLQuery {\n \"\"\"\n The key or name of the query parameter.\n \"\"\"\n key: String!\n \"\"\"\n Determines whether to ignore query parameters with empty values.\n \"\"\"\n skipEmpty: Boolean\n \"\"\"\n The actual value or a mustache template to resolve the value dynamically for the \n query parameter.\n \"\"\"\n value: String!\n}\n\ninput Schema {\n Obj: JSON\n Arr: Schema\n Opt: Schema\n Enum: [String!]\n}\n\n\"\"\"\nType to configure Cross-Origin Resource Sharing (CORS) for a server.\n\"\"\"\ninput Cors {\n \"\"\"\n Indicates whether the server allows credentials (e.g., cookies, authorization headers) \n to be sent in cross-origin requests.\n \"\"\"\n allowCredentials: Boolean\n \"\"\"\n A list of allowed headers in cross-origin requests. This can be used to specify custom \n headers that are allowed to be included in cross-origin requests.\n \"\"\"\n allowHeaders: [String!]\n \"\"\"\n A list of allowed HTTP methods in cross-origin requests. These methods specify the \n actions that are permitted in cross-origin requests.\n \"\"\"\n allowMethods: [Method]\n \"\"\"\n A list of origins that are allowed to access the server's resources in cross-origin \n requests. An origin can be a domain, a subdomain, or even 'null' for local file schemes.\n \"\"\"\n allowOrigins: [String!]\n \"\"\"\n Indicates whether requests from private network addresses are allowed in cross-origin \n requests. Private network addresses typically include IP addresses reserved for internal \n networks.\n \"\"\"\n allowPrivateNetwork: Boolean\n \"\"\"\n A list of headers that the server exposes to the browser in cross-origin responses. \n Exposing certain headers allows the client-side code to access them in the response.\n \"\"\"\n exposeHeaders: [String!]\n \"\"\"\n The maximum time (in seconds) that the client should cache preflight OPTIONS requests \n in order to avoid sending excessive requests to the server.\n \"\"\"\n maxAge: Int\n \"\"\"\n A list of header names that indicate the values of which might cause the server's \n response to vary, potentially affecting caching.\n \"\"\"\n vary: [String!]\n}\n\ninput Headers {\n \"\"\"\n `cacheControl` sends `Cache-Control` headers in responses when activated. The `max-age` \n value is the least of the values received from upstream services. @default `false`.\n \"\"\"\n cacheControl: Boolean\n \"\"\"\n `cors` allows Cross-Origin Resource Sharing (CORS) for a server.\n \"\"\"\n cors: Cors\n \"\"\"\n `headers` are key-value pairs included in every server response. Useful for setting \n headers like `Access-Control-Allow-Origin` for cross-origin requests or additional \n headers for downstream services.\n \"\"\"\n custom: [KeyValue]\n \"\"\"\n `experimental` allows the use of `X-*` experimental headers in the response. @default \n `[]`.\n \"\"\"\n experimental: [String!]\n \"\"\"\n `setCookies` when enabled stores `set-cookie` headers and all the response will be \n sent with the headers.\n \"\"\"\n setCookies: Boolean\n}\n\ninput Routes {\n graphQL: String!\n status: String!\n}\n\ninput ScriptOptions {\n timeout: Int\n}\n\ninput Apollo {\n \"\"\"\n Setting `apiKey` for Apollo.\n \"\"\"\n apiKey: String!\n \"\"\"\n Setting `graphRef` for Apollo in the format @.\n \"\"\"\n graphRef: String!\n \"\"\"\n Setting `platform` for Apollo.\n \"\"\"\n platform: String\n \"\"\"\n Setting `userVersion` for Apollo.\n \"\"\"\n userVersion: String\n \"\"\"\n Setting `version` for Apollo.\n \"\"\"\n version: String\n}\n\n\"\"\"\nOutput the opentelemetry data to otlp collector\n\"\"\"\ninput OtlpExporter {\n headers: [KeyValue]\n url: String!\n}\n\n\"\"\"\nOutput the telemetry metrics data to prometheus server\n\"\"\"\ninput PrometheusExporter {\n format: PrometheusFormat\n path: String!\n}\n\n\"\"\"\nOutput the opentelemetry data to the stdout. Mostly used for debug purposes\n\"\"\"\ninput StdoutExporter {\n \"\"\"\n Output to stdout in pretty human-readable format\n \"\"\"\n pretty: Boolean!\n}\n\ninput TelemetryExporter {\n stdout: StdoutExporter\n otlp: OtlpExporter\n prometheus: PrometheusExporter\n apollo: Apollo\n}\n\ninput Batch {\n delay: Int!\n headers: [String!]\n maxSize: Int\n}\n\ninput Proxy {\n url: String!\n}\n\n\"\"\"\nThe @graphQL operator allows to specify GraphQL API server request to fetch data \nfrom.\n\"\"\"\ninput GraphQL {\n \"\"\"\n Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args)\n \"\"\"\n args: [KeyValue]\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n If the upstream GraphQL server supports request batching, you can specify the 'batch' \n argument to batch several requests into a single batch request.Make sure you have \n also specified batch settings to the `@upstream` and to the `@graphQL` operator.\n \"\"\"\n batch: Boolean!\n \"\"\"\n The headers parameter allows you to customize the headers of the GraphQL request \n made by the `@graphQL` operator. It is used by specifying a key-value map of header \n names and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Specifies the root field on the upstream to request data from. This maps a field \n in your schema to a field in the upstream schema. When a query is received for this \n field, Tailcall requests data from the corresponding upstream field.\n \"\"\"\n name: String!\n}\n\n\"\"\"\nThe @grpc operator indicates that a field or node is backed by a gRPC API.For instance, \nif you add the @grpc operator to the `users` field of the Query type with a service \nargument of `NewsService` and method argument of `GetAllNews`, it signifies that \nthe `users` field is backed by a gRPC API. The `service` argument specifies the name \nof the gRPC service. The `method` argument specifies the name of the gRPC method. \nIn this scenario, the GraphQL server will make a gRPC request to the gRPC endpoint \nspecified when the `users` field is queried.\n\"\"\"\ninput Grpc {\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n This refers to the arguments of your gRPC call. You can pass it as a static object \n or use Mustache template for dynamic parameters. These parameters will be added in \n the body in `protobuf` format.\n \"\"\"\n body: JSON\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@grpc` operator. It is used by specifying a key-value map of header names \n and their values. Note: content-type is automatically set to application/grpc\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n This refers to the gRPC method you're going to call. For instance `GetAllNews`.\n \"\"\"\n method: String!\n}\n\n\"\"\"\nThe @http operator indicates that a field or node is backed by a REST API.For instance, \nif you add the @http operator to the `users` field of the Query type with a path \nargument of `\"/users\"`, it signifies that the `users` field is backed by a REST API. \nThe path argument specifies the path of the REST API. In this scenario, the GraphQL \nserver will make a GET request to the API endpoint specified when the `users` field \nis queried.\n\"\"\"\ninput Http {\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n The body of the API call. It's used for methods like POST or PUT that send data to \n the server. You can pass it as a static object or use a Mustache template to substitute \n variables from the GraphQL variables.\n \"\"\"\n body: String\n \"\"\"\n The `encoding` parameter specifies the encoding of the request body. It can be `ApplicationJson` \n or `ApplicationXWwwFormUrlEncoded`. @default `ApplicationJson`.\n \"\"\"\n encoding: Encoding\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@http` operator. It is used by specifying a key-value map of header names \n and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Schema of the input of the API call. It is automatically inferred in most cases.\n \"\"\"\n input: Schema\n \"\"\"\n This refers to the HTTP method of the API call. Commonly used methods include `GET`, \n `POST`, `PUT`, `DELETE` etc. @default `GET`.\n \"\"\"\n method: Method\n \"\"\"\n onRequest field in @http directive gives the ability to specify the request interception \n handler.\n \"\"\"\n onRequest: String\n \"\"\"\n Schema of the output of the API call. It is automatically inferred in most cases.\n \"\"\"\n output: Schema\n \"\"\"\n This refers to the API endpoint you're going to call. For instance `https://jsonplaceholder.typicode.com/users`.For \n dynamic segments in your API endpoint, use Mustache templates for variable substitution. \n For instance, to fetch a specific user, use `/users/{{args.id}}`.\n \"\"\"\n path: String!\n \"\"\"\n This represents the query parameters of your API call. You can pass it as a static \n object or use Mustache template for dynamic parameters. These parameters will be \n added to the URL. NOTE: Query parameter order is critical for batching in Tailcall. \n The first parameter referencing a field in the current value using mustache syntax \n is automatically selected as the batching parameter.\n \"\"\"\n query: [URLQuery]\n}\n\n\"\"\"\nThe `@expr` operators allows you to specify an expression that can evaluate to a \nvalue. The expression can be a static value or built form a Mustache template. schema.\n\"\"\"\ninput Expr {\n body: JSON\n}\n\ninput JS {\n name: String!\n}\n\ninput Modify {\n name: String\n omit: Boolean\n}\n\n\"\"\"\nThe @cache operator enables caching for the query, field or type it is applied to.\n\"\"\"\ninput Cache {\n \"\"\"\n Specifies the duration, in milliseconds, of how long the value has to be stored in \n the cache.\n \"\"\"\n maxAge: Int!\n}\n\n\"\"\"\nThe @telemetry directive facilitates seamless integration with OpenTelemetry, enhancing \nthe observability of your GraphQL services powered by Tailcall. By leveraging this \ndirective, developers gain access to valuable insights into the performance and behavior \nof their applications.\n\"\"\"\ninput Telemetry {\n export: TelemetryExporter\n \"\"\"\n The list of headers that will be sent as additional attributes to telemetry exporters \n Be careful about **leaking sensitive information** from requests when enabling the \n headers that may contain sensitive data\n \"\"\"\n requestHeaders: [String!]\n}\n\nenum Encoding {\n ApplicationJson\n ApplicationXWwwFormUrlencoded\n}\n\nenum Method {\n GET\n POST\n PUT\n PATCH\n DELETE\n HEAD\n OPTIONS\n CONNECT\n TRACE\n}\n\nenum LinkType {\n Config\n Protobuf\n Script\n Cert\n Key\n Operation\n Htpasswd\n Jwks\n Grpc\n}\n\nenum HttpVersion {\n HTTP1\n HTTP2\n}\n\n\"\"\"\nOutput format for prometheus data\n\"\"\"\nenum PrometheusFormat {\n text\n protobuf\n}\n\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n" + } + } + } +} diff --git a/tests/core/snapshots/apollo-federation-entities-batch.md_client.snap b/tests/core/snapshots/apollo-federation-entities-batch.md_client.snap new file mode 100644 index 0000000000..bf6bfe42a1 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities-batch.md_client.snap @@ -0,0 +1,73 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar DateTime + +scalar Email + +scalar Empty + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Post { + id: Int! + title: String! +} + +type Query { + """ + Apollo federation Query._entities resolver + """ + _entities(representations: [_Any!]!): [_Entity]! + """ + Apollo federation Query._service resolver + """ + _service: _Service! + user(id: Int!): User +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +type User { + id: Int! + name: String! +} + +scalar _Any + +union _Entity = Post | User + +type _Service { + sdl: String +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/apollo-federation-entities-batch.md_merged.snap b/tests/core/snapshots/apollo-federation-entities-batch.md_merged.snap new file mode 100644 index 0000000000..1403201548 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities-batch.md_merged.snap @@ -0,0 +1,43 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema + @server(port: 8000) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42) { + query: Query +} + +scalar _Any + +union _Entity = Post | User + +type Post + @graphQL(args: [{key: "id", value: "{{.value.id}}"}], baseURL: "http://upstream/graphql", batch: true, name: "post") + @key(fields: "id") { + id: Int! + title: String! +} + +type Query { + """ + Apollo federation Query._entities resolver + """ + _entities(representations: [_Any!]!): [_Entity]! + """ + Apollo federation Query._service resolver + """ + _service: _Service! + user(id: Int!): User @http(path: "/users/{{.args.id}}") +} + +type User + @http(batchKey: ["id"], path: "/users", query: [{key: "id", value: "{{.value.user.id}}"}]) + @key(fields: "user { id }") { + id: Int! + name: String! +} + +type _Service { + sdl: String +} diff --git a/tests/core/snapshots/apollo-federation-entities-validation.md_error.snap b/tests/core/snapshots/apollo-federation-entities-validation.md_error.snap new file mode 100644 index 0000000000..876c8e3ed9 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities-validation.md_error.snap @@ -0,0 +1,23 @@ +--- +source: tests/core/spec.rs +expression: errors +--- +[ + { + "message": "Type resolver can't use `.args`, use `.value` instead", + "trace": [ + "Post", + "http", + "query" + ], + "description": null + }, + { + "message": "Type resolver can't use `.args`, use `.value` instead", + "trace": [ + "User", + "call" + ], + "description": null + } +] diff --git a/tests/core/snapshots/apollo-federation-entities.md_0.snap b/tests/core/snapshots/apollo-federation-entities.md_0.snap new file mode 100644 index 0000000000..071a6837f3 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities.md_0.snap @@ -0,0 +1,36 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "_entities": [ + { + "__typename": "User", + "id": 1, + "name": "Leanne Graham" + }, + { + "__typename": "User", + "id": 2, + "name": "Ervin Howell" + }, + { + "__typename": "Post", + "id": 3, + "title": "post-title-3" + }, + { + "__typename": "Post", + "id": 5, + "title": "post-title-5" + } + ] + } + } +} diff --git a/tests/core/snapshots/apollo-federation-entities.md_1.snap b/tests/core/snapshots/apollo-federation-entities.md_1.snap new file mode 100644 index 0000000000..33868f4d32 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities.md_1.snap @@ -0,0 +1,17 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "_service": { + "sdl": "schema @server(port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @expr(body: {id: \"{{.value.id}}\", title: \"post-title-{{.value.id}}\"}) @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @call(steps: [{query: \"user\", args: {id: \"{{.value.user.id}}\"}}]) @key(fields: \"user { id }\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\n\"\"\"\nThe @addField operator simplifies data structures and queries by adding a field that \ninlines or flattens a nested field or node within your schema. more info [here](https://tailcall.run/docs/guides/operators/#addfield)\n\"\"\"\ndirective @addField(\n \"\"\"\n Name of the new field to be added\n \"\"\"\n name: String!\n \"\"\"\n Path of the data where the field should point to\n \"\"\"\n path: [String!]\n) repeatable on OBJECT\n\n\"\"\"\nThe @alias directive indicates that aliases of one enum value.\n\"\"\"\ndirective @alias(\n options: [String!]\n) on ENUM_VALUE\n\n\"\"\"\nThe @cache operator enables caching for the query, field or type it is applied to.\n\"\"\"\ndirective @cache(\n \"\"\"\n Specifies the duration, in milliseconds, of how long the value has to be stored in \n the cache.\n \"\"\"\n maxAge: Int!\n) on OBJECT | FIELD_DEFINITION\n\n\"\"\"\nProvides the ability to refer to multiple fields in the Query or Mutation root.\n\"\"\"\ndirective @call(\n \"\"\"\n Steps are composed together to form a call. If you have multiple steps, the output \n of the previous step is passed as input to the next step.\n \"\"\"\n steps: [Step]\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe `@expr` operators allows you to specify an expression that can evaluate to a \nvalue. The expression can be a static value or built form a Mustache template. schema.\n\"\"\"\ndirective @expr(\n body: JSON\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe @graphQL operator allows to specify GraphQL API server request to fetch data \nfrom.\n\"\"\"\ndirective @graphQL(\n \"\"\"\n Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args)\n \"\"\"\n args: [KeyValue]\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n If the upstream GraphQL server supports request batching, you can specify the 'batch' \n argument to batch several requests into a single batch request.Make sure you have \n also specified batch settings to the `@upstream` and to the `@graphQL` operator.\n \"\"\"\n batch: Boolean!\n \"\"\"\n The headers parameter allows you to customize the headers of the GraphQL request \n made by the `@graphQL` operator. It is used by specifying a key-value map of header \n names and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Specifies the root field on the upstream to request data from. This maps a field \n in your schema to a field in the upstream schema. When a query is received for this \n field, Tailcall requests data from the corresponding upstream field.\n \"\"\"\n name: String!\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe @grpc operator indicates that a field or node is backed by a gRPC API.For instance, \nif you add the @grpc operator to the `users` field of the Query type with a service \nargument of `NewsService` and method argument of `GetAllNews`, it signifies that \nthe `users` field is backed by a gRPC API. The `service` argument specifies the name \nof the gRPC service. The `method` argument specifies the name of the gRPC method. \nIn this scenario, the GraphQL server will make a gRPC request to the gRPC endpoint \nspecified when the `users` field is queried.\n\"\"\"\ndirective @grpc(\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n This refers to the arguments of your gRPC call. You can pass it as a static object \n or use Mustache template for dynamic parameters. These parameters will be added in \n the body in `protobuf` format.\n \"\"\"\n body: JSON\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@grpc` operator. It is used by specifying a key-value map of header names \n and their values. Note: content-type is automatically set to application/grpc\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n This refers to the gRPC method you're going to call. For instance `GetAllNews`.\n \"\"\"\n method: String!\n) on FIELD_DEFINITION | OBJECT\n\n\"\"\"\nThe @http operator indicates that a field or node is backed by a REST API.For instance, \nif you add the @http operator to the `users` field of the Query type with a path \nargument of `\"/users\"`, it signifies that the `users` field is backed by a REST API. \nThe path argument specifies the path of the REST API. In this scenario, the GraphQL \nserver will make a GET request to the API endpoint specified when the `users` field \nis queried.\n\"\"\"\ndirective @http(\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n The body of the API call. It's used for methods like POST or PUT that send data to \n the server. You can pass it as a static object or use a Mustache template to substitute \n variables from the GraphQL variables.\n \"\"\"\n body: String\n \"\"\"\n The `encoding` parameter specifies the encoding of the request body. It can be `ApplicationJson` \n or `ApplicationXWwwFormUrlEncoded`. @default `ApplicationJson`.\n \"\"\"\n encoding: Encoding\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@http` operator. It is used by specifying a key-value map of header names \n and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Schema of the input of the API call. It is automatically inferred in most cases.\n \"\"\"\n input: Schema\n \"\"\"\n This refers to the HTTP method of the API call. Commonly used methods include `GET`, \n `POST`, `PUT`, `DELETE` etc. @default `GET`.\n \"\"\"\n method: Method\n \"\"\"\n onRequest field in @http directive gives the ability to specify the request interception \n handler.\n \"\"\"\n onRequest: String\n \"\"\"\n Schema of the output of the API call. It is automatically inferred in most cases.\n \"\"\"\n output: Schema\n \"\"\"\n This refers to the API endpoint you're going to call. For instance `https://jsonplaceholder.typicode.com/users`.For \n dynamic segments in your API endpoint, use Mustache templates for variable substitution. \n For instance, to fetch a specific user, use `/users/{{args.id}}`.\n \"\"\"\n path: String!\n \"\"\"\n This represents the query parameters of your API call. You can pass it as a static \n object or use Mustache template for dynamic parameters. These parameters will be \n added to the URL. NOTE: Query parameter order is critical for batching in Tailcall. \n The first parameter referencing a field in the current value using mustache syntax \n is automatically selected as the batching parameter.\n \"\"\"\n query: [URLQuery]\n) on FIELD_DEFINITION | OBJECT\n\ndirective @js(\n name: String!\n) on FIELD_DEFINITION | OBJECT\n\ndirective @modify(\n name: String\n omit: Boolean\n) on FIELD_DEFINITION\n\n\"\"\"\nUsed to omit a field from public consumption.\n\"\"\"\ndirective @omit on FIELD_DEFINITION\n\ndirective @protected on OBJECT | FIELD_DEFINITION\n\n\"\"\"\nThe `@server` directive, when applied at the schema level, offers a comprehensive \nset of server configurations. It dictates how the server behaves and helps tune tailcall \nfor various use-cases.\n\"\"\"\ndirective @server(\n \"\"\"\n `apolloTracing` exposes GraphQL query performance data, including execution time \n of queries and individual resolvers.\n \"\"\"\n apolloTracing: Boolean\n \"\"\"\n `batchRequests` combines multiple requests into one, improving performance but potentially \n introducing latency and complicating debugging. Use judiciously. @default `false`.\n \"\"\"\n batchRequests: Boolean\n \"\"\"\n Enables deduplication of IO operations to enhance performance.This flag prevents \n duplicate IO requests from being executed concurrently, reducing resource load. Caution: \n May lead to issues with APIs that expect unique results for identical inputs, such \n as nonce-based APIs.\n \"\"\"\n dedupe: Boolean\n enableJIT: Boolean\n \"\"\"\n `globalResponseTimeout` sets the maximum query duration before termination, acting \n as a safeguard against long-running queries.\n \"\"\"\n globalResponseTimeout: Int\n \"\"\"\n `headers` contains key-value pairs that are included as default headers in server \n responses, allowing for consistent header management across all responses.\n \"\"\"\n headers: Headers\n \"\"\"\n `hostname` sets the server hostname.\n \"\"\"\n hostname: String\n \"\"\"\n `introspection` allows clients to fetch schema information directly, aiding tools \n and applications in understanding available types, fields, and operations. @default \n `true`.\n \"\"\"\n introspection: Boolean\n \"\"\"\n `pipelineFlush` allows to control flushing behavior of the server pipeline.\n \"\"\"\n pipelineFlush: Boolean\n \"\"\"\n `port` sets the Tailcall running port. @default `8000`.\n \"\"\"\n port: Int\n \"\"\"\n `queryValidation` checks incoming GraphQL queries against the schema, preventing \n errors from invalid queries. Can be disabled for performance. @default `false`.\n \"\"\"\n queryValidation: Boolean\n \"\"\"\n `responseValidation` Tailcall automatically validates responses from upstream services \n using inferred schema. @default `false`.\n \"\"\"\n responseValidation: Boolean\n \"\"\"\n `routes` allows customization of server endpoint paths. It provides options to change \n the default paths for status and GraphQL endpoints. Default values are: - status: \n \"/status\" - graphQL: \"/graphql\" If not specified, these default values will be used.\n \"\"\"\n routes: Routes\n \"\"\"\n A link to an external JS file that listens on every HTTP request response event.\n \"\"\"\n script: ScriptOptions\n \"\"\"\n `showcase` enables the /showcase/graphql endpoint.\n \"\"\"\n showcase: Boolean\n \"\"\"\n This configuration defines local variables for server operations. Useful for storing \n constant configurations, secrets, or shared information.\n \"\"\"\n vars: [KeyValue]\n \"\"\"\n `version` sets the HTTP version for the server. Options are `HTTP1` and `HTTP2`. \n @default `HTTP1`.\n \"\"\"\n version: HttpVersion\n \"\"\"\n `workers` sets the number of worker threads. @default the number of system cores.\n \"\"\"\n workers: Int\n) on SCHEMA\n\n\"\"\"\nThe @telemetry directive facilitates seamless integration with OpenTelemetry, enhancing \nthe observability of your GraphQL services powered by Tailcall. By leveraging this \ndirective, developers gain access to valuable insights into the performance and behavior \nof their applications.\n\"\"\"\ndirective @telemetry(\n export: TelemetryExporter\n \"\"\"\n The list of headers that will be sent as additional attributes to telemetry exporters \n Be careful about **leaking sensitive information** from requests when enabling the \n headers that may contain sensitive data\n \"\"\"\n requestHeaders: [String!]\n) on SCHEMA\n\n\"\"\"\nThe `upstream` directive allows you to control various aspects of the upstream server \nconnection. This includes settings like connection timeouts, keep-alive intervals, \nand more. If not specified, default values are used.\n\"\"\"\ndirective @upstream(\n \"\"\"\n `allowedHeaders` defines the HTTP headers allowed to be forwarded to upstream services. \n If not set, no headers are forwarded, enhancing security but possibly limiting data \n flow.\n \"\"\"\n allowedHeaders: [String!]\n \"\"\"\n This refers to the default base URL for your APIs. If it's not explicitly mentioned \n in the `@upstream` operator, then each [@http](#http) operator must specify its own \n `baseURL`. If neither `@upstream` nor [@http](#http) provides a `baseURL`, it results \n in a compilation error.\n \"\"\"\n baseURL: String\n \"\"\"\n An object that specifies the batch settings, including `maxSize` (the maximum size \n of the batch), `delay` (the delay in milliseconds between each batch), and `headers` \n (an array of HTTP headers to be included in the batch).\n \"\"\"\n batch: Batch\n \"\"\"\n The time in seconds that the connection will wait for a response before timing out.\n \"\"\"\n connectTimeout: Int\n \"\"\"\n The `http2Only` setting allows you to specify whether the client should always issue \n HTTP2 requests, without checking if the server supports it or not. By default it \n is set to `false` for all HTTP requests made by the server, but is automatically \n set to true for GRPC.\n \"\"\"\n http2Only: Boolean\n \"\"\"\n Providing httpCache size enables Tailcall's HTTP caching, adhering to the [HTTP Caching \n RFC](https://tools.ietf.org/html/rfc7234), to enhance performance by minimizing redundant \n data fetches. Defaults to `0` if unspecified.\n \"\"\"\n httpCache: Int\n \"\"\"\n The time in seconds between each keep-alive message sent to maintain the connection.\n \"\"\"\n keepAliveInterval: Int\n \"\"\"\n The time in seconds that the connection will wait for a keep-alive message before \n closing.\n \"\"\"\n keepAliveTimeout: Int\n \"\"\"\n A boolean value that determines whether keep-alive messages should be sent while \n the connection is idle.\n \"\"\"\n keepAliveWhileIdle: Boolean\n \"\"\"\n onRequest field gives the ability to specify the global request interception handler.\n \"\"\"\n onRequest: String\n \"\"\"\n The time in seconds that the connection pool will wait before closing idle connections.\n \"\"\"\n poolIdleTimeout: Int\n \"\"\"\n The maximum number of idle connections that will be maintained per host.\n \"\"\"\n poolMaxIdlePerHost: Int\n \"\"\"\n The `proxy` setting defines an intermediary server through which the upstream requests \n will be routed before reaching their intended endpoint. By specifying a proxy URL, \n you introduce an additional layer, enabling custom routing and security policies.\n \"\"\"\n proxy: Proxy\n \"\"\"\n The time in seconds between each TCP keep-alive message sent to maintain the connection.\n \"\"\"\n tcpKeepAlive: Int\n \"\"\"\n The maximum time in seconds that the connection will wait for a response.\n \"\"\"\n timeout: Int\n \"\"\"\n The User-Agent header value to be used in HTTP requests. @default `Tailcall/1.0`\n \"\"\"\n userAgent: String\n) on SCHEMA\n\n\"\"\"\nField whose value is a sequence of bytes.\n\"\"\"\nscalar Bytes\n\n\"\"\"\nField whose value conforms to the standard date format as specified in RFC 3339 (https://datatracker.ietf.org/doc/html/rfc3339).\n\"\"\"\nscalar Date\n\n\"\"\"\nField whose value conforms to the standard internet email address format as specified \nin HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address.\n\"\"\"\nscalar Email\n\n\"\"\"\nEmpty scalar type represents an empty value.\n\"\"\"\nscalar Empty\n\n\"\"\"\nField whose value is a 128-bit signed integer.\n\"\"\"\nscalar Int128\n\n\"\"\"\nField whose value is a 16-bit signed integer.\n\"\"\"\nscalar Int16\n\n\"\"\"\nField whose value is a 32-bit signed integer.\n\"\"\"\nscalar Int32\n\n\"\"\"\nField whose value is a 64-bit signed integer.\n\"\"\"\nscalar Int64\n\n\"\"\"\nField whose value is an 8-bit signed integer.\n\"\"\"\nscalar Int8\n\n\"\"\"\nField whose value conforms to the standard JSON format as specified in RFC 8259 (https://datatracker.ietf.org/doc/html/rfc8259).\n\"\"\"\nscalar JSON\n\n\"\"\"\nField whose value conforms to the standard E.164 format as specified in E.164 specification \n(https://en.wikipedia.org/wiki/E.164).\n\"\"\"\nscalar PhoneNumber\n\n\"\"\"\nField whose value is a 128-bit unsigned integer.\n\"\"\"\nscalar UInt128\n\n\"\"\"\nField whose value is a 16-bit unsigned integer.\n\"\"\"\nscalar UInt16\n\n\"\"\"\nField whose value is a 32-bit unsigned integer.\n\"\"\"\nscalar UInt32\n\n\"\"\"\nField whose value is a 64-bit unsigned integer.\n\"\"\"\nscalar UInt64\n\n\"\"\"\nField whose value is an 8-bit unsigned integer.\n\"\"\"\nscalar UInt8\n\n\"\"\"\nField whose value conforms to the standard URL format as specified in RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986).\n\"\"\"\nscalar Url\n\n\"\"\"\nProvides the ability to refer to a field defined in the root Query or Mutation.\n\"\"\"\ninput Step {\n \"\"\"\n The arguments that will override the actual arguments of the field.\n \"\"\"\n args: JSON\n \"\"\"\n The name of the field on the `Mutation` type that you want to call.\n \"\"\"\n mutation: String\n \"\"\"\n The name of the field on the `Query` type that you want to call.\n \"\"\"\n query: String\n}\n\ninput KeyValue {\n key: String!\n value: String!\n}\n\n\"\"\"\nThe URLQuery input type represents a query parameter to be included in a URL.\n\"\"\"\ninput URLQuery {\n \"\"\"\n The key or name of the query parameter.\n \"\"\"\n key: String!\n \"\"\"\n Determines whether to ignore query parameters with empty values.\n \"\"\"\n skipEmpty: Boolean\n \"\"\"\n The actual value or a mustache template to resolve the value dynamically for the \n query parameter.\n \"\"\"\n value: String!\n}\n\ninput Schema {\n Obj: JSON\n Arr: Schema\n Opt: Schema\n Enum: [String!]\n}\n\n\"\"\"\nType to configure Cross-Origin Resource Sharing (CORS) for a server.\n\"\"\"\ninput Cors {\n \"\"\"\n Indicates whether the server allows credentials (e.g., cookies, authorization headers) \n to be sent in cross-origin requests.\n \"\"\"\n allowCredentials: Boolean\n \"\"\"\n A list of allowed headers in cross-origin requests. This can be used to specify custom \n headers that are allowed to be included in cross-origin requests.\n \"\"\"\n allowHeaders: [String!]\n \"\"\"\n A list of allowed HTTP methods in cross-origin requests. These methods specify the \n actions that are permitted in cross-origin requests.\n \"\"\"\n allowMethods: [Method]\n \"\"\"\n A list of origins that are allowed to access the server's resources in cross-origin \n requests. An origin can be a domain, a subdomain, or even 'null' for local file schemes.\n \"\"\"\n allowOrigins: [String!]\n \"\"\"\n Indicates whether requests from private network addresses are allowed in cross-origin \n requests. Private network addresses typically include IP addresses reserved for internal \n networks.\n \"\"\"\n allowPrivateNetwork: Boolean\n \"\"\"\n A list of headers that the server exposes to the browser in cross-origin responses. \n Exposing certain headers allows the client-side code to access them in the response.\n \"\"\"\n exposeHeaders: [String!]\n \"\"\"\n The maximum time (in seconds) that the client should cache preflight OPTIONS requests \n in order to avoid sending excessive requests to the server.\n \"\"\"\n maxAge: Int\n \"\"\"\n A list of header names that indicate the values of which might cause the server's \n response to vary, potentially affecting caching.\n \"\"\"\n vary: [String!]\n}\n\ninput Headers {\n \"\"\"\n `cacheControl` sends `Cache-Control` headers in responses when activated. The `max-age` \n value is the least of the values received from upstream services. @default `false`.\n \"\"\"\n cacheControl: Boolean\n \"\"\"\n `cors` allows Cross-Origin Resource Sharing (CORS) for a server.\n \"\"\"\n cors: Cors\n \"\"\"\n `headers` are key-value pairs included in every server response. Useful for setting \n headers like `Access-Control-Allow-Origin` for cross-origin requests or additional \n headers for downstream services.\n \"\"\"\n custom: [KeyValue]\n \"\"\"\n `experimental` allows the use of `X-*` experimental headers in the response. @default \n `[]`.\n \"\"\"\n experimental: [String!]\n \"\"\"\n `setCookies` when enabled stores `set-cookie` headers and all the response will be \n sent with the headers.\n \"\"\"\n setCookies: Boolean\n}\n\ninput Routes {\n graphQL: String!\n status: String!\n}\n\ninput ScriptOptions {\n timeout: Int\n}\n\ninput Apollo {\n \"\"\"\n Setting `apiKey` for Apollo.\n \"\"\"\n apiKey: String!\n \"\"\"\n Setting `graphRef` for Apollo in the format @.\n \"\"\"\n graphRef: String!\n \"\"\"\n Setting `platform` for Apollo.\n \"\"\"\n platform: String\n \"\"\"\n Setting `userVersion` for Apollo.\n \"\"\"\n userVersion: String\n \"\"\"\n Setting `version` for Apollo.\n \"\"\"\n version: String\n}\n\n\"\"\"\nOutput the opentelemetry data to otlp collector\n\"\"\"\ninput OtlpExporter {\n headers: [KeyValue]\n url: String!\n}\n\n\"\"\"\nOutput the telemetry metrics data to prometheus server\n\"\"\"\ninput PrometheusExporter {\n format: PrometheusFormat\n path: String!\n}\n\n\"\"\"\nOutput the opentelemetry data to the stdout. Mostly used for debug purposes\n\"\"\"\ninput StdoutExporter {\n \"\"\"\n Output to stdout in pretty human-readable format\n \"\"\"\n pretty: Boolean!\n}\n\ninput TelemetryExporter {\n stdout: StdoutExporter\n otlp: OtlpExporter\n prometheus: PrometheusExporter\n apollo: Apollo\n}\n\ninput Batch {\n delay: Int!\n headers: [String!]\n maxSize: Int\n}\n\ninput Proxy {\n url: String!\n}\n\n\"\"\"\nThe @graphQL operator allows to specify GraphQL API server request to fetch data \nfrom.\n\"\"\"\ninput GraphQL {\n \"\"\"\n Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args)\n \"\"\"\n args: [KeyValue]\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n If the upstream GraphQL server supports request batching, you can specify the 'batch' \n argument to batch several requests into a single batch request.Make sure you have \n also specified batch settings to the `@upstream` and to the `@graphQL` operator.\n \"\"\"\n batch: Boolean!\n \"\"\"\n The headers parameter allows you to customize the headers of the GraphQL request \n made by the `@graphQL` operator. It is used by specifying a key-value map of header \n names and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Specifies the root field on the upstream to request data from. This maps a field \n in your schema to a field in the upstream schema. When a query is received for this \n field, Tailcall requests data from the corresponding upstream field.\n \"\"\"\n name: String!\n}\n\n\"\"\"\nThe @grpc operator indicates that a field or node is backed by a gRPC API.For instance, \nif you add the @grpc operator to the `users` field of the Query type with a service \nargument of `NewsService` and method argument of `GetAllNews`, it signifies that \nthe `users` field is backed by a gRPC API. The `service` argument specifies the name \nof the gRPC service. The `method` argument specifies the name of the gRPC method. \nIn this scenario, the GraphQL server will make a gRPC request to the gRPC endpoint \nspecified when the `users` field is queried.\n\"\"\"\ninput Grpc {\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n This refers to the arguments of your gRPC call. You can pass it as a static object \n or use Mustache template for dynamic parameters. These parameters will be added in \n the body in `protobuf` format.\n \"\"\"\n body: JSON\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@grpc` operator. It is used by specifying a key-value map of header names \n and their values. Note: content-type is automatically set to application/grpc\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n This refers to the gRPC method you're going to call. For instance `GetAllNews`.\n \"\"\"\n method: String!\n}\n\n\"\"\"\nThe @http operator indicates that a field or node is backed by a REST API.For instance, \nif you add the @http operator to the `users` field of the Query type with a path \nargument of `\"/users\"`, it signifies that the `users` field is backed by a REST API. \nThe path argument specifies the path of the REST API. In this scenario, the GraphQL \nserver will make a GET request to the API endpoint specified when the `users` field \nis queried.\n\"\"\"\ninput Http {\n \"\"\"\n This refers to the base URL of the API. If not specified, the default base URL is \n the one specified in the `@upstream` operator.\n \"\"\"\n baseURL: String\n \"\"\"\n The `batchKey` dictates the path Tailcall will follow to group the returned items \n from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).\n \"\"\"\n batchKey: [String!]\n \"\"\"\n The body of the API call. It's used for methods like POST or PUT that send data to \n the server. You can pass it as a static object or use a Mustache template to substitute \n variables from the GraphQL variables.\n \"\"\"\n body: String\n \"\"\"\n The `encoding` parameter specifies the encoding of the request body. It can be `ApplicationJson` \n or `ApplicationXWwwFormUrlEncoded`. @default `ApplicationJson`.\n \"\"\"\n encoding: Encoding\n \"\"\"\n The `headers` parameter allows you to customize the headers of the HTTP request made \n by the `@http` operator. It is used by specifying a key-value map of header names \n and their values.\n \"\"\"\n headers: [KeyValue]\n \"\"\"\n Schema of the input of the API call. It is automatically inferred in most cases.\n \"\"\"\n input: Schema\n \"\"\"\n This refers to the HTTP method of the API call. Commonly used methods include `GET`, \n `POST`, `PUT`, `DELETE` etc. @default `GET`.\n \"\"\"\n method: Method\n \"\"\"\n onRequest field in @http directive gives the ability to specify the request interception \n handler.\n \"\"\"\n onRequest: String\n \"\"\"\n Schema of the output of the API call. It is automatically inferred in most cases.\n \"\"\"\n output: Schema\n \"\"\"\n This refers to the API endpoint you're going to call. For instance `https://jsonplaceholder.typicode.com/users`.For \n dynamic segments in your API endpoint, use Mustache templates for variable substitution. \n For instance, to fetch a specific user, use `/users/{{args.id}}`.\n \"\"\"\n path: String!\n \"\"\"\n This represents the query parameters of your API call. You can pass it as a static \n object or use Mustache template for dynamic parameters. These parameters will be \n added to the URL. NOTE: Query parameter order is critical for batching in Tailcall. \n The first parameter referencing a field in the current value using mustache syntax \n is automatically selected as the batching parameter.\n \"\"\"\n query: [URLQuery]\n}\n\n\"\"\"\nThe `@expr` operators allows you to specify an expression that can evaluate to a \nvalue. The expression can be a static value or built form a Mustache template. schema.\n\"\"\"\ninput Expr {\n body: JSON\n}\n\ninput JS {\n name: String!\n}\n\ninput Modify {\n name: String\n omit: Boolean\n}\n\n\"\"\"\nThe @cache operator enables caching for the query, field or type it is applied to.\n\"\"\"\ninput Cache {\n \"\"\"\n Specifies the duration, in milliseconds, of how long the value has to be stored in \n the cache.\n \"\"\"\n maxAge: Int!\n}\n\n\"\"\"\nThe @telemetry directive facilitates seamless integration with OpenTelemetry, enhancing \nthe observability of your GraphQL services powered by Tailcall. By leveraging this \ndirective, developers gain access to valuable insights into the performance and behavior \nof their applications.\n\"\"\"\ninput Telemetry {\n export: TelemetryExporter\n \"\"\"\n The list of headers that will be sent as additional attributes to telemetry exporters \n Be careful about **leaking sensitive information** from requests when enabling the \n headers that may contain sensitive data\n \"\"\"\n requestHeaders: [String!]\n}\n\nenum Encoding {\n ApplicationJson\n ApplicationXWwwFormUrlencoded\n}\n\nenum Method {\n GET\n POST\n PUT\n PATCH\n DELETE\n HEAD\n OPTIONS\n CONNECT\n TRACE\n}\n\nenum LinkType {\n Config\n Protobuf\n Script\n Cert\n Key\n Operation\n Htpasswd\n Jwks\n Grpc\n}\n\nenum HttpVersion {\n HTTP1\n HTTP2\n}\n\n\"\"\"\nOutput format for prometheus data\n\"\"\"\nenum PrometheusFormat {\n text\n protobuf\n}\n\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n" + } + } + } +} diff --git a/tests/core/snapshots/apollo-federation-entities.md_client.snap b/tests/core/snapshots/apollo-federation-entities.md_client.snap new file mode 100644 index 0000000000..bf6bfe42a1 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities.md_client.snap @@ -0,0 +1,73 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar DateTime + +scalar Email + +scalar Empty + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Post { + id: Int! + title: String! +} + +type Query { + """ + Apollo federation Query._entities resolver + """ + _entities(representations: [_Any!]!): [_Entity]! + """ + Apollo federation Query._service resolver + """ + _service: _Service! + user(id: Int!): User +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +type User { + id: Int! + name: String! +} + +scalar _Any + +union _Entity = Post | User + +type _Service { + sdl: String +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/apollo-federation-entities.md_merged.snap b/tests/core/snapshots/apollo-federation-entities.md_merged.snap new file mode 100644 index 0000000000..b88afd6fb4 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-entities.md_merged.snap @@ -0,0 +1,40 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema + @server(port: 8000) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42) + @link(src: "./posts.graphql", type: Config) { + query: Query +} + +scalar _Any + +union _Entity = Post | User + +type Post @expr(body: {id: "{{.value.id}}", title: "post-title-{{.value.id}}"}) @key(fields: "id") { + id: Int! + title: String! +} + +type Query { + """ + Apollo federation Query._entities resolver + """ + _entities(representations: [_Any!]!): [_Entity]! + """ + Apollo federation Query._service resolver + """ + _service: _Service! + user(id: Int!): User @http(path: "/users/{{.args.id}}") +} + +type User @call(steps: [{query: "user", args: {id: "{{.value.user.id}}"}}]) @key(fields: "user { id }") { + id: Int! + name: String! +} + +type _Service { + sdl: String +} diff --git a/tests/execution/apollo-federation-entities-batch.md b/tests/execution/apollo-federation-entities-batch.md new file mode 100644 index 0000000000..94c378a52a --- /dev/null +++ b/tests/execution/apollo-federation-entities-batch.md @@ -0,0 +1,116 @@ +# Apollo federation query for batching resolvers + +```graphql @config +schema + @server(port: 8000) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) { + query: Query +} + +type Query { + user(id: Int!): User @http(path: "/users/{{.args.id}}") +} + +type User @http(path: "/users", query: [{key: "id", value: "{{.value.user.id}}"}], batchKey: ["id"]) { + id: Int! + name: String! +} + +type Post + @graphQL(baseURL: "http://upstream/graphql", batch: true, name: "post", args: [{key: "id", value: "{{.value.id}}"}]) { + id: Int! + title: String! +} +``` + +```yml @mock +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users?id=1&id=2 + assertHits: false + response: + status: 200 + body: + - id: 1 + name: Leanne Graham + - id: 2 + name: Ervin Howell + +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users?id=2&id=1 + assertHits: false + response: + status: 200 + body: + - id: 2 + name: Ervin Howell + - id: 1 + name: Leanne Graham + +- request: + method: POST + url: http://upstream/graphql + textBody: '[{ "query": "query { post(id: 3) { id title } }" },{ "query": "query { post(id: 5) { id title } }" }]' + assertHits: false + response: + status: 200 + body: + - data: + post: + id: 3 + title: ea molestias quasi exercitationem repellat qui ipsa sit aut + - data: + post: + id: 5 + title: nesciunt quas odio + +- request: + method: POST + url: http://upstream/graphql + textBody: '[{ "query": "query { post(id: 5) { id title } }" },{ "query": "query { post(id: 3) { id title } }" }]' + assertHits: false + response: + status: 200 + body: + - data: + post: + id: 5 + title: nesciunt quas odio + - data: + post: + id: 3 + title: ea molestias quasi exercitationem repellat qui ipsa sit aut +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: > + { + _entities(representations: [ + {user: { id: 1 }, __typename: "User"} + {user: { id: 2 }, __typename: "User"} + # TODO: fix selection set of fields for @graphQL directive in jit + # {id: 3, __typename: "Post"} + # {id: 5, __typename: "Post"} + ]) { + __typename + ...on User { + id + name + } + ...on Post { + id + title + } + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: > + { _service { sdl } } +``` diff --git a/tests/execution/apollo-federation-entities-validation.md b/tests/execution/apollo-federation-entities-validation.md new file mode 100644 index 0000000000..2d43c04af4 --- /dev/null +++ b/tests/execution/apollo-federation-entities-validation.md @@ -0,0 +1,78 @@ +--- +error: true +--- + +# Apollo federation query validation + +```graphql @config +schema + @server(port: 8000) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) { + query: Query +} + +type Query { + user(id: Int!): User @http(path: "/users/{{.args.id}}") +} + +type User @call(steps: [{query: "user", args: {id: "{{.args.id}}"}}]) { + id: Int! + name: String! +} + +type Post @http(path: "/users", query: [{key: "id", value: "{{.args.user.id}}"}]) { + id: Int! + title: String! +} +``` + +```yml @mock +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users/1 + response: + status: 200 + body: + id: 1 + name: Leanne Graham + +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users/2 + response: + status: 200 + body: + id: 2 + name: Ervin Howell +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: > + { + _entities(representations: [ + {user: { id: 1 }, __typename: "User"} + {user: { id: 2 }, __typename: "User"} + {user: { id: 3 }, __typename: "Post"} + {user: { id: 5 }, __typename: "Post"} + ]) { + __typename + ...on User { + id + name + } + ...on Post { + id + title + } + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: > + { _service { sdl } } +``` diff --git a/tests/execution/apollo-federation-entities.md b/tests/execution/apollo-federation-entities.md new file mode 100644 index 0000000000..8ee332ce6e --- /dev/null +++ b/tests/execution/apollo-federation-entities.md @@ -0,0 +1,77 @@ +# Apollo federation query + +```graphql @config +schema + @server(port: 8000) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) + @link(src: "./posts.graphql") { + query: Query +} + +type Query { + user(id: Int!): User @http(path: "/users/{{.args.id}}") +} + +type User @call(steps: [{query: "user", args: {id: "{{.value.user.id}}"}}]) { + id: Int! + name: String! +} +``` + +```graphql @file:posts.graphql +type Post @expr(body: {id: "{{.value.id}}", title: "post-title-{{.value.id}}"}) { + id: Int! + title: String! +} +``` + +```yml @mock +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users/1 + response: + status: 200 + body: + id: 1 + name: Leanne Graham + +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users/2 + response: + status: 200 + body: + id: 2 + name: Ervin Howell +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: > + { + _entities(representations: [ + {user: { id: 1 }, __typename: "User"} + {user: { id: 2 }, __typename: "User"} + {id: 3, __typename: "Post"} + {id: 5, __typename: "Post"} + ]) { + __typename + ...on User { + id + name + } + ...on Post { + id + title + } + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: > + { _service { sdl } } +```