diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ef8dcd0..49f3ee7b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -154,4 +154,4 @@ jobs: if: ${{ github.event_name == 'pull_request' }} with: filePath: tools/panic_free_analyzer/output.md - comment_tag: rust-code-audit + comment_tag: rust-code-audit \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4f3dfefb..b1928821 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -55,7 +55,6 @@ jobs: set: | *.cache-from=type=gha,scope=build *.cache-to=type=gha,scope=build,mode=max - - name: docker details pr comment uses: marocchino/sticky-pull-request-comment@v2 if: ${{ github.event_name == 'pull_request' }} diff --git a/Cargo.lock b/Cargo.lock index 3bf05d93..cd3b5998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "actix-codec" version = "0.5.1" @@ -319,6 +329,12 @@ dependencies = [ "term", ] +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -392,6 +408,80 @@ dependencies = [ "once_cell", ] +[[package]] +name = "async-graphql" +version = "6.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298a5d587d6e6fdb271bf56af2dc325a80eb291fd0fc979146584b9a05494a8c" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64 0.13.1", + "bytes", + "fast_chemail", + "fnv", + "futures-util", + "handlebars", + "http", + "indexmap 2.1.0", + "mime", + "multer", + "num-traits", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions", + "tempfile", + "thiserror", +] + +[[package]] +name = "async-graphql-derive" +version = "6.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f329c7eb9b646a72f70c9c4b516c70867d356ec46cb00dcac8ad343fd006b0" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "strum", + "syn 2.0.46", + "thiserror", +] + +[[package]] +name = "async-graphql-parser" +version = "6.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6139181845757fd6a73fbb8839f3d036d7150b798db0e9bb3c6e83cdd65bd53b" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "6.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323a5143f5bdd2030f45e3f2e0c821c9b1d36e79cf382129c64299c50a7f3750" +dependencies = [ + "bytes", + "indexmap 2.1.0", + "serde", + "serde_json", +] + [[package]] name = "async-io" version = "1.13.0" @@ -523,6 +613,28 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + [[package]] name = "async-task" version = "4.6.0" @@ -771,6 +883,9 @@ name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] [[package]] name = "bytestring" @@ -1100,22 +1215,28 @@ dependencies = [ name = "conductor_engine" version = "0.0.0" dependencies = [ + "anyhow", "async-trait", + "base64 0.21.5", "conductor_common", "conductor_config", "cors_plugin", "disable_introspection_plugin", + "federation_query_planner", "futures", "graphiql_plugin", "http_get_plugin", + "humantime", "jwt_auth_plugin", "match_content_type_plugin", "reqwest", "serde", "serde_json", "thiserror", + "tokio", "tracing", "trusted_documents_plugin", + "ureq", "vrl", "vrl_plugin", "wasm_polyfills", @@ -1132,6 +1253,18 @@ dependencies = [ "napi-derive", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1365,6 +1498,41 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.46", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.46", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -1506,6 +1674,12 @@ dependencies = [ "log", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1569,6 +1743,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1584,6 +1767,26 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "federation_query_planner" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-graphql", + "async-trait", + "criterion", + "futures", + "graphql-parser", + "insta", + "lazy_static", + "linked-hash-map", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1857,6 +2060,20 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "handlebars" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2059,6 +2276,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -2077,7 +2300,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -2088,6 +2310,7 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.2", + "serde", ] [[package]] @@ -2106,6 +2329,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "yaml-rust", +] + [[package]] name = "instant" version = "0.1.12" @@ -2486,6 +2723,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "napi" version = "2.14.2" @@ -3450,6 +3705,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -3496,7 +3773,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", - "indexmap 1.9.3", "schemars_derive", "serde", "serde_json", @@ -3520,6 +3796,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "seahash" version = "4.1.0" @@ -3825,6 +4111,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.7" @@ -3847,6 +4139,34 @@ dependencies = [ "vte", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.46", +] + [[package]] name = "subtle" version = "2.5.0" @@ -4344,6 +4664,22 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64 0.21.5", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "url", + "webpki-roots", +] + [[package]] name = "url" version = "2.5.0" @@ -4643,6 +4979,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 8afeb075..0699d49e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,13 @@ tracing = "0.1.40" http = "0.2.11" http-body = "0.4.6" bytes = "1.5.0" -async-trait = "0.1.77" -anyhow = "1.0.79" -reqwest = "0.11.23" -thiserror = "1.0.56" +async-trait = "0.1.74" +anyhow = "1.0.75" +reqwest = { version = "0.11.22" } +thiserror = "1.0.50" tracing-subscriber = "0.3.18" -schemars = { version = "0.8.16", features = ["preserve_order"] } +base64 = "0.21.5" +schemars = "0.8.16" vrl = { git = "https://github.com/vectordotdev/vrl.git", rev = "afaca43e3ed266827e0921d1790a7f11e0abc0f0", default-features = false, features = [ "string_path", "compiler", diff --git a/bin/cloudflare_worker/pnpm-lock.yaml b/bin/cloudflare_worker/pnpm-lock.yaml index b31722cc..02023500 100644 --- a/bin/cloudflare_worker/pnpm-lock.yaml +++ b/bin/cloudflare_worker/pnpm-lock.yaml @@ -306,14 +306,14 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@types/node-forge@1.3.10: - resolution: {integrity: sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==} + /@types/node-forge@1.3.11: + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.10.7 dev: true - /@types/node@20.10.5: - resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==} + /@types/node@20.10.7: + resolution: {integrity: sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==} dependencies: undici-types: 5.26.5 dev: true @@ -323,8 +323,8 @@ packages: engines: {node: '>=0.4.0'} dev: true - /acorn@8.11.2: - resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -523,7 +523,7 @@ packages: hasBin: true dependencies: '@cspotcode/source-map-support': 0.8.1 - acorn: 8.11.2 + acorn: 8.11.3 acorn-walk: 8.3.1 capnp-ts: 0.7.0 exit-hook: 2.2.1 @@ -615,7 +615,7 @@ packages: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} dependencies: - '@types/node-forge': 1.3.10 + '@types/node-forge': 1.3.11 node-forge: 1.3.1 dev: true diff --git a/libs/config/conductor.schema.json b/libs/config/conductor.schema.json index f0db77ad..f15207bb 100644 --- a/libs/config/conductor.schema.json +++ b/libs/config/conductor.schema.json @@ -8,16 +8,12 @@ "sources" ], "properties": { - "server": { - "description": "Configuration for the HTTP server.", - "anyOf": [ - { - "$ref": "#/definitions/ServerConfig" - }, - { - "type": "null" - } - ] + "endpoints": { + "description": "List of GraphQL endpoints to be exposed by the gateway. Each endpoint is a GraphQL schema that is backed by one or more sources and can have a unique set of plugins applied to.\n\nFor additional information, please refer to the [Endpoints section](./endpoints).", + "type": "array", + "items": { + "$ref": "#/definitions/EndpointDefinition" + } }, "logger": { "description": "Conductor logger configuration.", @@ -30,20 +26,6 @@ } ] }, - "sources": { - "description": "List of sources to be used by the gateway. Each source is a GraphQL endpoint or multiple endpoints grouped using a federated implementation.\n\nFor additional information, please refer to the [Sources section](./sources/graphql).", - "type": "array", - "items": { - "$ref": "#/definitions/SourceDefinition" - } - }, - "endpoints": { - "description": "List of GraphQL endpoints to be exposed by the gateway. Each endpoint is a GraphQL schema that is backed by one or more sources and can have a unique set of plugins applied to.\n\nFor additional information, please refer to the [Endpoints section](./endpoints).", - "type": "array", - "items": { - "$ref": "#/definitions/EndpointDefinition" - } - }, "plugins": { "description": "List of global plugins to be applied to all endpoints. Global plugins are applied before endpoint-specific plugins.", "type": [ @@ -53,146 +35,206 @@ "items": { "$ref": "#/definitions/PluginDefinition" } - } - }, - "definitions": { - "ServerConfig": { - "type": "object", - "properties": { - "port": { - "description": "The port to listen on, default to 9000", - "default": 9000, - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "host": { - "description": "The host to listen on, default to 127.0.0.1", - "default": "127.0.0.1", - "type": "string" - } - } }, - "LoggerConfig": { - "type": "object", - "properties": { - "level": { - "description": "Log level", - "default": "info", - "$ref": "#/definitions/Level" + "server": { + "description": "Configuration for the HTTP server.", + "anyOf": [ + { + "$ref": "#/definitions/ServerConfig" + }, + { + "type": "null" } - } - }, - "Level": { - "type": "string", - "enum": [ - "trace", - "debug", - "info", - "warn", - "error" ] }, - "SourceDefinition": { - "description": "A source definition for a GraphQL endpoint or a federated GraphQL implementation.", - "oneOf": [ + "sources": { + "description": "List of sources to be used by the gateway. Each source is a GraphQL endpoint or multiple endpoints grouped using a federated implementation.\n\nFor additional information, please refer to the [Sources section](./sources/graphql).", + "type": "array", + "items": { + "$ref": "#/definitions/SourceDefinition" + } + } + }, + "definitions": { + "CorsPluginConfig": { + "description": "The `cors` plugin enables [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) configuration for your GraphQL API.\n\nBy using this plugin, you can define rules for allowing cross-origin requests to your GraphQL server. This is essential for web applications that need to interact with your API from different domains.", + "examples": [ { - "description": "A simple, single GraphQL endpoint", - "type": "object", - "required": [ - "config", - "id", - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "graphql" - ] - }, - "id": { - "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", - "type": "string" - }, - "config": { - "description": "The configuration for the GraphQL source.", - "$ref": "#/definitions/GraphQLSourceConfig" - } + "$metadata": { + "title": "Strict CORS", + "description": "This example demonstrates how to configure the CORS plugin with a strict list of methods, headers and origins." + }, + "type": "cors", + "enabled": true, + "config": { + "max_age": 3600, + "allow_credentials": true, + "allowed_methods": "GET, POST", + "allowed_origin": "https://example.com", + "allowed_headers": "Content-Type, Authorization", + "allow_private_network": false } }, { - "description": "A simple, single GraphQL endpoint", - "type": "object", - "required": [ - "config", - "id", - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "mock" - ] - }, - "id": { - "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", - "type": "string" - }, - "config": { - "description": "The configuration for the mocked source.", - "$ref": "#/definitions/MockedSourceConfig" - } + "$metadata": { + "title": "Permissive CORS", + "description": "This example demonstrates how to configure the CORS plugin with a permissive setup." + }, + "type": "cors", + "enabled": true, + "config": { + "max_age": 3600, + "allow_credentials": true, + "allowed_methods": "*", + "allowed_origin": "*", + "allowed_headers": "*", + "exposed_headers": "*", + "allow_private_network": true } - } - ] - }, - "GraphQLSourceConfig": { - "description": "An upstream based on a simple, single GraphQL endpoint.\n\nBy using this source, you can easily wrap an existing GraphQL upstream server, and enrich it with features and plugins.", - "examples": [ + }, { "$metadata": { - "title": "Simple", - "description": null + "title": "Reflect Origin", + "description": "This example demonstrates how to configure the CORS plugin with a reflect Origin setup." }, - "type": "graphql", - "id": "my-source", + "type": "cors", + "enabled": true, "config": { - "endpoint": "https://my-source.com/graphql" + "max_age": 3600, + "allow_credentials": true, + "allowed_methods": "GET, POST", + "allowed_origin": "reflect", + "allowed_headers": "*", + "exposed_headers": "*", + "allow_private_network": false } } ], "type": "object", - "required": [ - "endpoint" - ], - "properties": { - "endpoint": { - "description": "The HTTP(S) endpoint URL for the GraphQL source.", - "type": "string" - } - } - }, - "MockedSourceConfig": { - "description": "A mocked upstream with a static response for all executed operations.", - "type": "object", - "required": [ - "response_data" - ], "properties": { - "response_data": { - "$ref": "#/definitions/LocalFileReference" + "allow_credentials": { + "description": "`Access-Control-Allow-Credentials`: Specifies whether to include credentials in the CORS headers. Credentials can include cookies, authorization headers, or TLS client certificates. Indicates whether the response to the request can be exposed when the credentials flag is true.", + "default": false, + "type": [ + "boolean", + "null" + ] + }, + "allow_private_network": { + "description": "`Access-Control-Allow-Private-Network`: Indicates whether requests from private networks are allowed when originating from public networks.", + "default": false, + "type": [ + "boolean", + "null" + ] + }, + "allowed_headers": { + "description": "`Access-Control-Allow-Headers`: Lists the headers allowed in actual requests. This helps in specifying which headers can be used when making the actual request. Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. You can also specify a special value \"*\" to allow any headers to be used when making the actual request, and the `Access-Control-Request-Headers` will be used from the incoming request.", + "default": "*", + "type": [ + "string", + "null" + ] + }, + "allowed_methods": { + "description": "`Access-Control-Allow-Methods`: Defines the HTTP methods allowed when accessing the resource. This is used in response to a CORS preflight request. Specifies the method or methods allowed when accessing the resource in response to a preflight request. You can also specify a special value \"*\" to allow any HTTP method to access the resource.", + "default": "*", + "type": [ + "string", + "null" + ] + }, + "allowed_origin": { + "description": "`Access-Control-Allow-Origin`: Determines which origins are allowed to access the resource. It can be a specific origin or a wildcard for allowing any origin. You can also specify a special value \"*\" to allow any origin to access the resource. You can also specify a special value \"reflect\" to allow the origin of the incoming request to access the resource.", + "default": "*", + "type": [ + "string", + "null" + ] + }, + "exposed_headers": { + "description": "`Access-Control-Expose-Headers`: The \"Access-Control-Expose-Headers\" response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request. You can also specify a special value \"*\" to allow any headers to be exposed to scripts running in the browser.", + "default": "*", + "type": [ + "string", + "null" + ] + }, + "max_age": { + "description": "`Access-Control-Max-Age`: Indicates how long the results of a preflight request can be cached. This field represents the duration in seconds.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } } }, - "LocalFileReference": { - "type": "string", - "format": "path" - }, - "EndpointDefinition": { - "description": "The `Endpoint` object exposes a GraphQL source with set of plugins applied to it.\n\nEach Endpoint can have its own set of plugins, which are applied after the global plugins. Endpoints can expose the same source with different plugins applied to it, to create different sets of features for different clients or consumers.", - "examples": [ + "DisableIntrospectionPluginConfig": { + "description": "The `disable_introspection` plugin allows you to disable introspection for your GraphQL API.\n\nA [GraphQL introspection query](https://graphql.org/learn/introspection/) is a special GraphQL query that returns information about the GraphQL schema of your API.\n\nIt it [recommended to disable introspection for production environments](https://escape.tech/blog/should-i-disable-introspection-in-graphql/), unless you have a specific use-case for it.\n\nIt can either disable introspection for all requests, or only for requests that match a specific condition (using VRL scripting language).", + "examples": [ + { + "$metadata": { + "title": "Disable Introspection", + "description": "This example disables introspection for all requests for the configured Endpoint." + }, + "type": "disable_introspection", + "enabled": true, + "config": {} + }, + { + "$metadata": { + "title": "Conditional", + "description": "This example disables introspection for all requests that doesn't have the \"bypass-introspection\" HTTP header." + }, + "type": "disable_introspection", + "enabled": true, + "config": { + "condition": { + "from": "inline", + "content": "%downstream_http_req.headers.\"bypass-introspection\" != \"1\"" + } + } + } + ], + "type": "object", + "properties": { + "condition": { + "description": "A VRL condition that determines whether to disable introspection for the request. This condition is evaluated only if the incoming GraphQL request is detected as an introspection query.\n\nThe condition is evaluated in the context of the incoming request and have access to the metadata field `%downstream_http_req` (fields: `body`, `uri`, `query_string`, `method`, `headers`).\n\nThe condition must return a boolean value: return `true` to continue and disable the introspection, and `false` to allow the introspection to run.\n\nIn case of a runtime error, or an unexpected return value, the script will be ignored and introspection will be disabled for the incoming request.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" + }, + { + "type": "null" + } + ] + } + } + }, + "Duration": { + "type": "object", + "required": [ + "nanos", + "secs" + ], + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "EndpointDefinition": { + "description": "The `Endpoint` object exposes a GraphQL source with set of plugins applied to it.\n\nEach Endpoint can have its own set of plugins, which are applied after the global plugins. Endpoints can expose the same source with different plugins applied to it, to create different sets of features for different clients or consumers.", + "examples": [ { "$metadata": { "title": "Basic Example", @@ -278,14 +320,14 @@ "path" ], "properties": { - "path": { - "description": "A valid HTTP path to listen on for this endpoint. This will be used for the main GraphQL endpoint as well as for the GraphiQL endpoint. In addition, plugins that extends the HTTP layer will use this path as a base path.", - "type": "string" - }, "from": { "description": "The identifier of the `Source` to be used.\n\nThis must match the `id` field of a `Source` definition.", "type": "string" }, + "path": { + "description": "A valid HTTP path to listen on for this endpoint. This will be used for the main GraphQL endpoint as well as for the GraphiQL endpoint. In addition, plugins that extends the HTTP layer will use this path as a base path.", + "type": "string" + }, "plugins": { "description": "A list of unique plugins to be applied to this endpoint. These plugins will be applied after the global plugins.\n\nOrder of plugins is important: plugins are applied in the order they are defined.", "type": [ @@ -298,826 +340,829 @@ } } }, - "PluginDefinition": { + "FederationSourceConfig": { + "type": "object", + "required": [ + "supergraph" + ], + "properties": { + "supergraph": { + "description": "The endpoint URL for the GraphQL source.", + "$ref": "#/definitions/SupergraphSourceConfig" + } + } + }, + "GraphQLSourceConfig": { + "description": "An upstream based on a simple, single GraphQL endpoint.\n\nBy using this source, you can easily wrap an existing GraphQL upstream server, and enrich it with features and plugins.", + "examples": [ + { + "$metadata": { + "title": "Simple", + "description": null + }, + "type": "graphql", + "id": "my-source", + "config": { + "endpoint": "https://my-source.com/graphql" + } + } + ], + "type": "object", + "required": [ + "endpoint" + ], + "properties": { + "endpoint": { + "description": "The HTTP(S) endpoint URL for the GraphQL source.", + "type": "string" + } + } + }, + "GraphiQLPluginConfig": { + "description": "This plugin adds a GraphiQL interface to your Endpoint.\n\nThis plugin is rendering the GraphiQL interface for HTTP `GET` requests, that are not intercepted by other plugins.", + "examples": [ + { + "$metadata": { + "title": "Enable GraphiQL", + "description": null + }, + "type": "graphiql", + "enabled": true, + "config": {} + } + ], + "type": "object", + "properties": { + "headers_editor_enabled": { + "description": "Enable/disable the HTTP headers editor in the GraphiQL interface.", + "default": true, + "type": [ + "boolean", + "null" + ] + } + } + }, + "HttpGetPluginConfig": { + "description": "The `http_get` plugin allows you to expose your GraphQL API over HTTP `GET` requests. This feature is fully compliant with the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/).\n\nBy enabling this plugin, you can execute GraphQL queries and mutations over HTTP `GET` requests, using HTTP query parameters, for example:\n\n`GET /graphql?query=query%20%7B%20__typename%20%7D`\n\n### Query Parameters\n\nFor complete documentation of the supported query parameters, see the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/#sec-GET).\n\n- `query`: The GraphQL query to execute\n\n- `variables` (optional): A JSON-encoded string containing the GraphQL variables\n\n- `operationName` (optional): The name of the GraphQL operation to execute\n\n### Headers\n\nTo execute GraphQL queries over HTTP `GET` requests, you must set the `Content-Type` header to `application/json`, **or** the `Accept` header to `application/x-www-form-urlencoded` / `application/graphql-response+json`.", + "examples": [ + { + "$metadata": { + "title": "Simple", + "description": null + }, + "type": "http_get", + "enabled": true, + "config": {} + }, + { + "$metadata": { + "title": "Enable Mutations", + "description": "This example enables mutations over HTTP GET requests." + }, + "type": "http_get", + "enabled": true, + "config": { + "mutations": true + } + } + ], + "type": "object", + "properties": { + "mutations": { + "description": "Allow mutations over GET requests.\n\n**The option is disabled by default:** this restriction is necessary to conform with the long-established semantics of safe methods within HTTP.", + "default": false, + "type": [ + "boolean", + "null" + ] + } + } + }, + "JwksProviderSourceConfig": { "oneOf": [ { + "title": "local", + "description": "A local file on the file-system. This file will be read once on startup and cached.", "type": "object", "required": [ - "type" + "path", + "source" ], "properties": { - "type": { + "path": { + "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", + "$ref": "#/definitions/LocalFileReference" + }, + "source": { "type": "string", "enum": [ - "graphiql" - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "config": { - "anyOf": [ - { - "$ref": "#/definitions/GraphiQLPluginConfig" - }, - { - "type": "null" - } + "local" ] } } }, { + "title": "remote", + "description": "A remote JWKS provider. The JWKS will be fetched via HTTP/HTTPS and cached.", "type": "object", "required": [ - "type" + "source", + "url" ], "properties": { - "type": { - "type": "string", - "enum": [ - "cors" - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "config": { + "cache_duration": { + "description": "Duration after which the cached JWKS should be expired. If not specified, the default value will be used.", + "default": "10m", "anyOf": [ { - "$ref": "#/definitions/CorsPluginConfig" + "$ref": "#/definitions/Duration" }, { "type": "null" } ] - } - } - }, - { - "description": "Configuration for the Disable Introspection plugin.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "disable_introspection" - ] }, - "enabled": { - "default": true, + "prefetch": { + "description": "If set to `true`, the JWKS will be fetched on startup and cached. In case of invalid JWKS, the error will be ignored and the plugin will try to fetch again when server receives the first request. If set to `false`, the JWKS will be fetched on-demand, when the first request comes in.", "type": [ "boolean", "null" ] }, - "config": { - "anyOf": [ - { - "$ref": "#/definitions/DisableIntrospectionPluginConfig" - }, - { - "type": "null" - } - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "http_get" - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "config": { - "anyOf": [ - { - "$ref": "#/definitions/HttpGetPluginConfig" - }, - { - "type": "null" - } - ] - } - } - }, - { - "type": "object", - "required": [ - "config", - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "vrl" - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "config": { - "$ref": "#/definitions/VrlPluginConfig" - } - } - }, - { - "type": "object", - "required": [ - "config", - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "trusted_documents" - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "config": { - "$ref": "#/definitions/TrustedDocumentsPluginConfig" - } - } - }, - { - "type": "object", - "required": [ - "config", - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "jwt_auth" - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" + "source": { + "type": "string", + "enum": [ + "remote" ] }, - "config": { - "$ref": "#/definitions/JwtAuthPluginConfig" + "url": { + "description": "The URL to fetch the JWKS key set from, via HTTP/HTTPS.", + "type": "string" } } } ] }, - "GraphiQLPluginConfig": { - "description": "This plugin adds a GraphiQL interface to your Endpoint.\n\nThis plugin is rendering the GraphiQL interface for HTTP `GET` requests, that are not intercepted by other plugins.", + "JwtAuthPluginConfig": { + "description": "The `jwt_auth` plugin implements the [JSON Web Tokens](https://jwt.io/introduction) specification.\n\nIt can be used to verify the JWT signature, and optionally validate the token issuer and audience. It can also forward the token and its claims to the upstream service.\n\nThe JWKS configuration can be either a local file on the file-system, or a remote JWKS provider.\n\nBy default, the plugin will look for the JWT token in the `Authorization` header, with the `Bearer` prefix.\n\nYou can also configure the plugin to reject requests that don't have a valid JWT token.", "examples": [ { "$metadata": { - "title": "Enable GraphiQL", - "description": null + "title": "Local JWKS", + "description": "This example is loading a JWKS file from the local file-system. The token is looked up in the `Authorization` header." }, - "type": "graphiql", + "type": "jwt_auth", "enabled": true, - "config": {} - } - ], - "type": "object", - "properties": { - "headers_editor_enabled": { - "description": "Enable/disable the HTTP headers editor in the GraphiQL interface.", - "default": true, - "type": [ - "boolean", - "null" - ] - } - } - }, - "CorsPluginConfig": { - "description": "The `cors` plugin enables [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) configuration for your GraphQL API.\n\nBy using this plugin, you can define rules for allowing cross-origin requests to your GraphQL server. This is essential for web applications that need to interact with your API from different domains.", - "examples": [ + "config": { + "lookup_locations": [ + { + "source": "header", + "name": "Authorization", + "prefix": "Bearer" + } + ], + "jwks_providers": [ + { + "source": "local", + "path": "jwks.json" + } + ] + } + }, { "$metadata": { - "title": "Strict CORS", - "description": "This example demonstrates how to configure the CORS plugin with a strict list of methods, headers and origins." + "title": "Remote JWKS with prefetch", + "description": "This example is loading a remote JWKS, when the server starts (prefetch). The token is looked up in the `Authorization` header." }, - "type": "cors", + "type": "jwt_auth", "enabled": true, "config": { - "max_age": 3600, - "allow_credentials": true, - "allowed_methods": "GET, POST", - "allowed_origin": "https://example.com", - "allowed_headers": "Content-Type, Authorization", - "allow_private_network": false + "lookup_locations": [ + { + "source": "header", + "name": "Authorization", + "prefix": "Bearer" + } + ], + "jwks_providers": [ + { + "source": "remote", + "url": "https://example.com/jwks.json", + "cache_duration": "10m", + "prefetch": true + } + ] } }, { "$metadata": { - "title": "Permissive CORS", - "description": "This example demonstrates how to configure the CORS plugin with a permissive setup." + "title": "Reject Unauthenticated", + "description": "This example is loading a remote JWKS, and looks for the token in the `auth` cookie. If the token is not present, the request will be rejected." }, - "type": "cors", + "type": "jwt_auth", "enabled": true, "config": { - "max_age": 3600, - "allow_credentials": true, - "allowed_methods": "*", - "allowed_origin": "*", - "allowed_headers": "*", - "exposed_headers": "*", - "allow_private_network": true + "reject_unauthenticated_requests": true, + "jwks_providers": [ + { + "source": "remote", + "url": "https://example.com/jwks.json", + "cache_duration": "10m", + "prefetch": true + } + ], + "lookup_locations": [ + { + "source": "cookies", + "name": "auth" + } + ] } }, { "$metadata": { - "title": "Reflect Origin", - "description": "This example demonstrates how to configure the CORS plugin with a reflect Origin setup." + "title": "Claims Forwarding", + "description": "This example is loading a remote JWKS, and looks for the token in the `jwt` cookie. If the token is not present, the request will be rejected. The token and its claims will be forwarded to the upstream service in the `X-Auth-Token` and `X-Auth-Claims` headers." }, - "type": "cors", + "type": "jwt_auth", "enabled": true, "config": { - "max_age": 3600, - "allow_credentials": true, - "allowed_methods": "GET, POST", - "allowed_origin": "reflect", - "allowed_headers": "*", - "exposed_headers": "*", - "allow_private_network": false + "forward_claims_to_upstream_header": "X-Auth-Claims", + "jwks_providers": [ + { + "source": "remote", + "url": "https://example.com/jwks.json", + "cache_duration": "10m", + "prefetch": true + } + ], + "lookup_locations": [ + { + "source": "cookies", + "name": "jwt" + } + ], + "reject_unauthenticated_requests": true, + "forward_token_to_upstream_header": "X-Auth-Token" } - } - ], - "type": "object", - "properties": { - "allow_credentials": { - "description": "`Access-Control-Allow-Credentials`: Specifies whether to include credentials in the CORS headers. Credentials can include cookies, authorization headers, or TLS client certificates. Indicates whether the response to the request can be exposed when the credentials flag is true.", - "default": false, - "type": [ - "boolean", - "null" - ] - }, - "allowed_methods": { - "description": "`Access-Control-Allow-Methods`: Defines the HTTP methods allowed when accessing the resource. This is used in response to a CORS preflight request. Specifies the method or methods allowed when accessing the resource in response to a preflight request. You can also specify a special value \"*\" to allow any HTTP method to access the resource.", - "default": "*", - "type": [ - "string", - "null" - ] }, - "allowed_origin": { - "description": "`Access-Control-Allow-Origin`: Determines which origins are allowed to access the resource. It can be a specific origin or a wildcard for allowing any origin. You can also specify a special value \"*\" to allow any origin to access the resource. You can also specify a special value \"reflect\" to allow the origin of the incoming request to access the resource.", - "default": "*", + { + "$metadata": { + "title": "Strict Validation", + "description": "This example is using strict validation, where the token issuer and audience are checked." + }, + "type": "jwt_auth", + "enabled": true, + "config": { + "lookup_locations": [ + { + "source": "cookies", + "name": "jwt" + } + ], + "jwks_providers": [ + { + "source": "remote", + "url": "https://example.com/jwks.json", + "cache_duration": "10m", + "prefetch": null + } + ], + "issuers": [ + "https://example.com" + ], + "audiences": [ + "realm.myapp.com" + ] + } + } + ], + "type": "object", + "required": [ + "jwks_providers" + ], + "properties": { + "allowed_algorithms": { + "description": "List of allowed algorithms for verifying the JWT signature. If not specified, the default list of all supported algorithms in [`jsonwebtoken` crate](https://crates.io/crates/jsonwebtoken) are used.", + "default": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "EdDSA" + ], "type": [ - "string", + "array", "null" - ] + ], + "items": { + "type": "string" + } }, - "allowed_headers": { - "description": "`Access-Control-Allow-Headers`: Lists the headers allowed in actual requests. This helps in specifying which headers can be used when making the actual request. Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. You can also specify a special value \"*\" to allow any headers to be used when making the actual request, and the `Access-Control-Request-Headers` will be used from the incoming request.", - "default": "*", + "audiences": { + "description": "The list of [JWT audiences](https://tools.ietf.org/html/rfc7519#section-4.1.3) are allowed to access. If this field is set, the token's `aud` field must be one of the values in this list, otherwise the token's `aud` field is not checked.", "type": [ - "string", + "array", "null" - ] + ], + "items": { + "type": "string" + } }, - "exposed_headers": { - "description": "`Access-Control-Expose-Headers`: The \"Access-Control-Expose-Headers\" response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request. You can also specify a special value \"*\" to allow any headers to be exposed to scripts running in the browser.", - "default": "*", + "forward_claims_to_upstream_header": { + "description": "Forward the JWT claims to the upstream service in the specified header.", "type": [ "string", "null" ] }, - "allow_private_network": { - "description": "`Access-Control-Allow-Private-Network`: Indicates whether requests from private networks are allowed when originating from public networks.", - "default": false, + "forward_token_to_upstream_header": { + "description": "Forward the JWT token to the upstream service in the specified header.", "type": [ - "boolean", + "string", "null" ] }, - "max_age": { - "description": "`Access-Control-Max-Age`: Indicates how long the results of a preflight request can be cached. This field represents the duration in seconds.", + "issuers": { + "description": "Specify the [principal](https://tools.ietf.org/html/rfc7519#section-4.1.1) that issued the JWT, usually a URL or an email address. If specified, it has to match the `iss` field in JWT, otherwise the token's `iss` field is not checked.", "type": [ - "integer", + "array", "null" ], - "format": "uint64", - "minimum": 0.0 - } - } - }, - "DisableIntrospectionPluginConfig": { - "description": "The `disable_introspection` plugin allows you to disable introspection for your GraphQL API.\n\nA [GraphQL introspection query](https://graphql.org/learn/introspection/) is a special GraphQL query that returns information about the GraphQL schema of your API.\n\nIt it [recommended to disable introspection for production environments](https://escape.tech/blog/should-i-disable-introspection-in-graphql/), unless you have a specific use-case for it.\n\nIt can either disable introspection for all requests, or only for requests that match a specific condition (using VRL scripting language).", - "examples": [ - { - "$metadata": { - "title": "Disable Introspection", - "description": "This example disables introspection for all requests for the configured Endpoint." - }, - "type": "disable_introspection", - "enabled": true, - "config": {} + "items": { + "type": "string" + } }, - { - "$metadata": { - "title": "Conditional", - "description": "This example disables introspection for all requests that doesn't have the \"bypass-introspection\" HTTP header." - }, - "type": "disable_introspection", - "enabled": true, - "config": { - "condition": { - "from": "inline", - "content": "%downstream_http_req.headers.\"bypass-introspection\" != \"1\"" - } + "jwks_providers": { + "description": "A list of JWKS providers to use for verifying the JWT signature. Can be either a path to a local JSON of the file-system, or a URL to a remote JWKS provider.", + "type": "array", + "items": { + "$ref": "#/definitions/JwksProviderSourceConfig" } - } - ], - "type": "object", - "properties": { - "condition": { - "description": "A VRL condition that determines whether to disable introspection for the request. This condition is evaluated only if the incoming GraphQL request is detected as an introspection query.\n\nThe condition is evaluated in the context of the incoming request and have access to the metadata field `%downstream_http_req` (fields: `body`, `uri`, `query_string`, `method`, `headers`).\n\nThe condition must return a boolean value: return `true` to continue and disable the introspection, and `false` to allow the introspection to run.\n\nIn case of a runtime error, or an unexpected return value, the script will be ignored and introspection will be disabled for the incoming request.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" - }, + }, + "lookup_locations": { + "description": "A list of locations to look up for the JWT token in the incoming HTTP request. The first one that is found will be used.", + "default": [ { - "type": "null" + "source": "header", + "name": "Authorization", + "prefix": "Bearer" } + ], + "type": "array", + "items": { + "$ref": "#/definitions/JwtAuthPluginLookupLocation" + } + }, + "reject_unauthenticated_requests": { + "description": "If set to `true`, the entire request will be rejected if the JWT token is not present in the request.", + "type": [ + "boolean", + "null" ] } } }, - "VrlConfigReference": { + "JwtAuthPluginLookupLocation": { "oneOf": [ { - "title": "inline", - "description": "Inline string for a VRL code snippet. The string is parsed and executed as a VRL plugin.", + "title": "header", "type": "object", "required": [ - "content", - "from" + "name", + "source" ], "properties": { - "from": { + "name": { + "type": "string" + }, + "prefix": { + "type": [ + "string", + "null" + ] + }, + "source": { "type": "string", "enum": [ - "inline" + "header" ] - }, - "content": { - "type": "string" } } }, { - "title": "file", - "description": "File reference to a VRL file. The file is loaded and executed as a VRL plugin.", + "title": "query_param", "type": "object", "required": [ - "from", - "path" + "name", + "source" ], "properties": { - "from": { + "name": { + "type": "string" + }, + "source": { "type": "string", "enum": [ - "file" + "query_param" ] - }, - "path": { - "$ref": "#/definitions/LocalFileReference" } } - } - ] - }, - "HttpGetPluginConfig": { - "description": "The `http_get` plugin allows you to expose your GraphQL API over HTTP `GET` requests. This feature is fully compliant with the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/).\n\nBy enabling this plugin, you can execute GraphQL queries and mutations over HTTP `GET` requests, using HTTP query parameters, for example:\n\n`GET /graphql?query=query%20%7B%20__typename%20%7D`\n\n### Query Parameters\n\nFor complete documentation of the supported query parameters, see the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/#sec-GET).\n\n- `query`: The GraphQL query to execute\n\n- `variables` (optional): A JSON-encoded string containing the GraphQL variables\n\n- `operationName` (optional): The name of the GraphQL operation to execute\n\n### Headers\n\nTo execute GraphQL queries over HTTP `GET` requests, you must set the `Content-Type` header to `application/json`, **or** the `Accept` header to `application/x-www-form-urlencoded` / `application/graphql-response+json`.", - "examples": [ - { - "$metadata": { - "title": "Simple", - "description": null - }, - "type": "http_get", - "enabled": true, - "config": {} }, { - "$metadata": { - "title": "Enable Mutations", - "description": "This example enables mutations over HTTP GET requests." - }, - "type": "http_get", - "enabled": true, - "config": { - "mutations": true + "title": "cookies", + "type": "object", + "required": [ + "name", + "source" + ], + "properties": { + "name": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "cookies" + ] + } } } - ], + ] + }, + "Level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warn", + "error" + ] + }, + "LocalFileReference": { + "type": "string", + "format": "path" + }, + "LoggerConfig": { "type": "object", "properties": { - "mutations": { - "description": "Allow mutations over GET requests.\n\n**The option is disabled by default:** this restriction is necessary to conform with the long-established semantics of safe methods within HTTP.", - "default": false, - "type": [ - "boolean", - "null" - ] + "level": { + "description": "Log level", + "default": "info", + "$ref": "#/definitions/Level" } } }, - "VrlPluginConfig": { - "description": "To simplify the process of extending the functionality of the GraphQL Gateway, we adopted a Rust-based script language called [VRL](https://vector.dev/docs/reference/vrl/).\n\nVRL language is intended for writing simple scripts that can be executed in the context of the GraphQL Gateway. VRL is focused around safety and performance: the script is compiled into Rust code when the server starts, and executed as a native Rust code ([you can find a comparison between VRL and other scripting languages here](https://github.com/YassinEldeeb/rust-embedded-langs-vs-native-benchmark)).\n\n> VRL was initially created to allow users to extend [Vector](https://vector.dev/), a high-performance observability data router, and adopted for Conductor to allow developers to extend the functionality of the GraphQL Gateway easily.\n\n### Writing VRL\n\nVRL is an expression-oriented language. A VRL program consists entirely of expressions, with every expression returning a value. You can define variables, call functions, and use operators to manipulate values.\n\n#### Variables and Functions\n\nThe following program defines a variable `myVar` with the value `\"myValue\"` and prints it to the console:\n\n```vrl\n\nmyVar = \"my value\"\n\nlog(myVar, level:\"info\")\n\n```\n\n#### Assignment\n\nThe `.` is used to set output values. In this example, we are setting the `x-authorization` header of the upstream HTTP request to `my-value`.\n\nHere's an example for a VRL program that extends Conductor's behavior by adding a custom HTTP header to all upstream HTTP requests:\n\n```vrl\n\n.upstream_http_req.headers.\"x-authorization\" = \"my-value\"\n\n```\n\n#### Metadata\n\nThe `%` is used to access metadata values. Note that metadata values are read only.\n\nThe following program is printing a metadata value to the console:\n\n```vrl\n\nlog(%downstream_http_req.headers.authorization, level:\"info\")\n\n```\n\n#### Further Reading\n\n- [VRL Playground](https://playground.vrl.dev/)\n\n- [VRL concepts documentation](https://vector.dev/docs/reference/vrl/#concepts)\n\n- [VRL syntax documentation](https://vector.dev/docs/reference/vrl/expressions/)\n\n- [Compiler errors documentation](https://vector.dev/docs/reference/vrl/errors/)\n\n- [VRL program examples](https://vector.dev/docs/reference/vrl/examples/)\n\n### Runtime Failure Handling\n\nSome VRL functions are fallible, meaning that they can error. Any potential errors thrown by fallible functions must be handled, a requirement enforced at compile time.\n\n```vrl\n\n# This function is fallible, and can create errors, so it must be handled.\n\nparsed, err = parse_json(\"invalid json\")\n\n```\n\nVRL function calls can be marked as infallible by adding a `!` suffix to the function call: (note that this might lead to runtime errors)\n\n```vrl\n\nparsed = parse_json!(\"invalid json\")\n\n```\n\n> In case of a runtime error of a fallible function call, an error will be returned to the end-user, and the gateway will not continue with the execution.\n\n### Input/Output\n\n#### `on_downstream_http_request`\n\nThe `on_downstream_http_request` hook is executed when a downstream HTTP request is received to the gateway from the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_req.body` (type: `string`): The body string of the incoming HTTP request.\n\n- `%downstream_http_req.uri` (type: `string`): The URI of the incoming HTTP request.\n\n- `%downstream_http_req.query_string` (type: `string`): The query string of the incoming HTTP request.\n\n- `%downstream_http_req.method` (type: `string`): The HTTP method of the incoming HTTP request.\n\n- `%downstream_http_req.headers` (type: `object`): The HTTP headers of the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, the gateway will skip the lookup phase, and will use this GraphQL operation instead.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can specify the executable operation by setting this value.\n\n- `.graphql.variables` (type: `object`): The GraphQL variables to be used when executing the GraphQL operation.\n\n- `.graphql.extensions` (type: `object`): The GraphQL extensions to be used when executing the GraphQL operation.\n\n#### `on_downstream_graphql_request`\n\nThe `on_downstream_graphql_request` hook is executed when a GraphQL operation is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_graphql_req.operation` (type: `string`): The GraphQL operation string, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.operation_name`(type: `string`) : If multiple GraphQL operations are set in `%downstream_graphql_req.operation`, you can specify the executable operation by setting this value.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, it will override the existing operation.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can override the extracted value by setting this field.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request. Setting this value will override the existing variables.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request. Setting this value will override the existing extensions.\n\n#### `on_upstream_http_request`\n\nThe `on_upstream_http_request` hook is executed when an HTTP request is about to be sent to the upstream GraphQL server.\n\nThe following metadata inputs are available to the hook:\n\n- `%upstream_http_req.body` (type: `string`): The body string of the planned HTTP request.\n\n- `%upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request.\n\n- `%upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request.\n\n- `%upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request.\n\n- `%upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request.\n\nThe following output values are available to the hook:\n\n- `.upstream_http_req.body` (type: `string`): The body string of the planned HTTP request. Setting this value will override the existing body.\n\n- `.upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request. Setting this value will override the existing URI.\n\n- `.upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request. Setting this value will override the existing query string.\n\n- `.upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request. Setting this value will override the existing HTTP method.\n\n- `.upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n#### `on_downstream_http_response`\n\nThe `on_downstream_http_response` hook is executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_res.body` (type: `string`): The body string of the HTTP response.\n\n- `%downstream_http_res.status` (type: `number`): The status code of the HTTP response.\n\n- `%downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response.\n\nThe following output values are available to the hook:\n\n- `.downstream_http_res.body` (type: `string`): The body string of the HTTP response. Setting this value will override the existing body.\n\n- `.downstream_http_res.status` (type: `number`): The status code of the HTTP response. Setting this value will override the existing status code.\n\n- `.downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n### Shared State\n\nDuring the execution of VRL programs, Conductor configures a shared state object for every incoming HTTP request.\n\nThis means that you can create type-safe shared state objects, and use them to share data between different VRL programs and hooks.\n\nYou can find an example for this in the **Examples** section below.\n\n### Available Functions", - "examples": [ - { - "$metadata": { - "title": "Inline", - "description": "Load and execute VRL plugins using inline configuration." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "inline", - "content": ".upstream_http_req.headers.\"x-authorization\" = \"some-value\"\n " - } - } - }, - { - "$metadata": { - "title": "File", - "description": "Load and execute VRL plugins using an external '.vrl' file." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "file", - "path": "my_plugin.vrl" - } - } - }, + "PluginDefinition": { + "oneOf": [ { - "$metadata": { - "title": "Headers Passthrough", - "description": "This example is using the shared-state feature to store the headers from the incoming HTTP request, and it pass it through to upstream calls." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "inline", - "content": ".upstream_http_req.headers = incoming_headers\n " + "type": "object", + "required": [ + "type" + ], + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/definitions/GraphiQLPluginConfig" + }, + { + "type": "null" + } + ] }, - "on_downstream_http_request": { - "from": "inline", - "content": "incoming_headers = %downstream_http_req.headers\n " + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "graphiql" + ] } } }, { - "$metadata": { - "title": "Shared State", - "description": "The following example is configuring a variable, and use it later" - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "inline", - "content": ".upstream_http_req.headers.\"x-auth\" = authorization_header\n " + "type": "object", + "required": [ + "type" + ], + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/definitions/CorsPluginConfig" + }, + { + "type": "null" + } + ] }, - "on_downstream_http_request": { - "from": "inline", - "content": "authorization_header = %downstream_http_req.headers.authorization\n " + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "cors" + ] } } }, { - "$metadata": { - "title": "Short Circuit", - "description": "The following example rejects all incoming requests that doesn't have the \"authorization\" header set." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_downstream_http_request": { - "from": "inline", - "content": "if %downstream_http_req.headers.authorization == null {\nshort_circuit!(403, \"Missing authorization header\")\n}\n " + "description": "Configuration for the Disable Introspection plugin.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/definitions/DisableIntrospectionPluginConfig" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "disable_introspection" + ] } } }, { - "$metadata": { - "title": "Custom GraphQL Extraction", - "description": "The following example is using a custom GraphQL extraction, overriding the default gateway behavior. In this example, we parse the incoming body as JSON and use the parsed value to find the GraphQL operation. Assuming the body structure is: `{ \"runThisQuery\": \"query { __typename }\", \"variables\": { }`." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_downstream_http_request": { - "from": "inline", - "content": "parsed_body = parse_json!(%downstream_http_req.body)\n.graphql.operation = parsed_body.runThisQuery\n.graphql.variables = parsed_body.variables\n " - } - } - } - ], - "type": "object", - "properties": { - "on_downstream_http_request": { - "description": "A hook executed when a downstream HTTP request is received to the gateway from the end-user. This hook allow you to extract information from the request, for later use, or to reject a request quickly.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" + "type": "object", + "required": [ + "type" + ], + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/definitions/HttpGetPluginConfig" + }, + { + "type": "null" + } + ] }, - { - "type": "null" - } - ] - }, - "on_downstream_graphql_request": { - "description": "A hook executed when a GraphQL query is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent. This hooks allow you to easily manipulate the incoming GraphQL request.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] }, - { - "type": "null" + "type": { + "type": "string", + "enum": [ + "http_get" + ] } - ] + } }, - "on_upstream_http_request": { - "description": "A hook executed when an HTTP request is about to be sent to the upstream GraphQL server. This hook allow you to manipulate upstream HTTP calls easily.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" + { + "type": "object", + "required": [ + "config", + "type" + ], + "properties": { + "config": { + "$ref": "#/definitions/VrlPluginConfig" }, - { - "type": "null" - } - ] - }, - "on_downstream_http_response": { - "description": "A hook executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user. This hook allow you to manipulate the end-user response easily.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] }, - { - "type": "null" - } - ] - } - } - }, - "TrustedDocumentsPluginConfig": { - "examples": [ - { - "$metadata": { - "title": "Local File Store", - "description": "This example is using a local file called `trusted_documents.json` as a store, using the Key->Value map format. The protocol exposed is based on HTTP `POST`, using the `documentId` parameter from the request body." - }, - "type": "trusted_documents", - "enabled": true, - "config": { - "protocols": [ - { - "type": "document_id", - "field_name": "documentId" - } - ], - "store": { - "source": "file", - "path": "trusted_documents.json", - "format": "json_key_value" + "type": { + "type": "string", + "enum": [ + "vrl" + ] } } }, { - "$metadata": { - "title": "HTTP GET", - "description": "This example uses a local file store called `trusted_documents.json`, using the Key->Value map format. The protocol exposed is based on HTTP `GET`, and extracts all parameters from the query string." - }, - "type": "trusted_documents", - "enabled": true, - "config": { - "protocols": [ - { - "type": "http_get", - "document_id_from": { - "source": "search_query", - "name": "documentId" - }, - "variables_from": { - "source": "search_query", - "name": "variables" - }, - "operation_name_from": { - "source": "search_query", - "name": "operationName" - } - } - ], - "store": { - "source": "file", - "path": "trusted_documents.json", - "format": "json_key_value" + "type": "object", + "required": [ + "config", + "type" + ], + "properties": { + "config": { + "$ref": "#/definitions/TrustedDocumentsPluginConfig" + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "trusted_documents" + ] } } - } - ], - "type": "object", - "required": [ - "protocols", - "store" - ], - "properties": { - "store": { - "description": "The store defines the source of trusted documents. The store contents is a list of hashes and GraphQL documents that are allowed to be executed.", - "$ref": "#/definitions/TrustedDocumentsPluginStoreConfig" }, - "protocols": { - "description": "A list of protocols to be exposed by this plugin. Each protocol defines how to obtain the document ID from the incoming request. You can specify multiple kinds of protocols, if needed.", - "type": "array", - "items": { - "$ref": "#/definitions/TrustedDocumentsProtocolConfig" - } - }, - "allow_untrusted": { - "description": "By default, this plugin does not allow untrusted operations to be executed. This is a security measure to prevent accidental exposure of operations that are not persisted.", - "type": [ - "boolean", - "null" - ] - } - } - }, - "TrustedDocumentsPluginStoreConfig": { - "oneOf": [ { - "title": "file", - "description": "File-based store configuration. The path specified is relative to the location of the root configuration file. The file contents are loaded into memory on startup. The file is not reloaded automatically. The file format is specified by the `format` field, based on the structure of your file.", "type": "object", - "required": [ - "format", - "path", - "source" + "required": [ + "config", + "type" ], "properties": { - "source": { + "config": { + "$ref": "#/definitions/JwtAuthPluginConfig" + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "type": { "type": "string", "enum": [ - "file" + "jwt_auth" ] - }, - "path": { - "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", - "$ref": "#/definitions/LocalFileReference" - }, - "format": { - "description": "The format and the expected structure of the loaded store file.", - "$ref": "#/definitions/TrustedDocumentsFileFormat" } } } ] }, - "TrustedDocumentsFileFormat": { - "oneOf": [ - { - "title": "apollo_persisted_query_manifest", - "description": "JSON file formated based on [Apollo Persisted Query Manifest](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#1-generate-operation-manifest).", - "type": "string", - "enum": [ - "apollo_persisted_query_manifest" - ] + "ServerConfig": { + "type": "object", + "properties": { + "host": { + "description": "The host to listen on, default to 127.0.0.1", + "default": "127.0.0.1", + "type": "string" }, - { - "title": "json_key_value", - "description": "A simple JSON map of key-value pairs.\n\nExample: `{\"key1\": \"query { __typename }\"}`", - "type": "string", - "enum": [ - "json_key_value" - ] + "port": { + "description": "The port to listen on, default to 9000", + "default": 9000, + "type": "integer", + "format": "uint16", + "minimum": 0.0 } - ] + } }, - "TrustedDocumentsProtocolConfig": { + "SourceDefinition": { + "description": "A source definition for a GraphQL endpoint or a federated GraphQL implementation.", "oneOf": [ { - "title": "apollo_manifest_extensions", - "description": "This protocol is based on [Apollo's Persisted Query Extensions](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#2-publish-operation-manifest). The GraphQL operation key is sent over `POST` and contains `extensions` field with the GraphQL document hash.\n\nExample: `POST /graphql {\"extensions\": {\"persistedQuery\": {\"version\": 1, \"sha256Hash\": \"123\"}}`", + "description": "A simple, single GraphQL endpoint", "type": "object", "required": [ + "config", + "id", "type" ], "properties": { + "config": { + "description": "The configuration for the GraphQL source.", + "$ref": "#/definitions/GraphQLSourceConfig" + }, + "id": { + "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", + "type": "string" + }, "type": { "type": "string", "enum": [ - "apollo_manifest_extensions" + "graphql" ] } } }, { - "title": "document_id", - "description": "This protocol is based on a `POST` request with a JSON body containing a field with the document ID. By default, the field name is `documentId`.\n\nExample: `POST /graphql {\"documentId\": \"123\", \"variables\": {\"code\": \"AF\"}, \"operationName\": \"test\"}`", + "description": "federation endpoint", "type": "object", "required": [ + "config", + "id", "type" ], "properties": { + "config": { + "description": "The configuration for the GraphQL source.", + "$ref": "#/definitions/FederationSourceConfig" + }, + "id": { + "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", + "type": "string" + }, "type": { "type": "string", "enum": [ - "document_id" + "federation" ] - }, - "field_name": { - "description": "The name of the JSON field containing the document ID in the incoming request.", - "default": "documentId", - "type": "string" } } + } + ] + }, + "SupergraphSourceConfig": { + "oneOf": [ + { + "description": "The file path for the Supergraph schema.", + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "$ref": "#/definitions/LocalFileReference" + } + }, + "additionalProperties": false }, { - "title": "http_get", - "description": "This protocol is based on a HTTP `GET` request. You can customize where to fetch each one of the parameters from. Each request parameter can be obtained from a different source: query, path, or header. By defualt, all parameters are obtained from the query string.\n\nUnlike other protocols, this protocol does not support sending GraphQL mutations.\n\nExample: `GET /graphql?documentId=123&variables=%7B%22code%22%3A%22AF%22%7D&operationName=test`", + "description": "The environment variable that contains the Supergraph schema.", "type": "object", "required": [ - "type" + "env" ], "properties": { - "type": { - "type": "string", - "enum": [ - "http_get" - ] - }, - "document_id_from": { - "description": "Instructions for fetching the document ID parameter from the incoming HTTP request.", - "default": { - "source": "search_query", - "name": "documentId" - }, - "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" - }, - "variables_from": { - "description": "Instructions for fetching the variables parameter from the incoming HTTP request. GraphQL variables must be passed as a JSON-encoded string.", - "default": { - "source": "search_query", - "name": "variables" - }, - "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" - }, - "operation_name_from": { - "description": "Instructions for fetching the operationName parameter from the incoming HTTP request.", - "default": { - "source": "search_query", - "name": "operationName" - }, - "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + "env": { + "type": "string" } - } + }, + "additionalProperties": false + }, + { + "description": "The remote endpoint where the Supergraph schema can be fetched.", + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "fetch_every": { + "type": [ + "string", + "null" + ] + }, + "headers": { + "description": "Optional headers to include in the request (ex: for authentication)", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "url": { + "description": "The URL endpoint from where to fetch the Supergraph schema.", + "type": "string" + } + } + } + }, + "additionalProperties": false } ] }, @@ -1132,15 +1177,15 @@ "source" ], "properties": { + "name": { + "description": "The name of the HTTP query parameter.", + "type": "string" + }, "source": { "type": "string", "enum": [ "search_query" ] - }, - "name": { - "description": "The name of the HTTP query parameter.", - "type": "string" } } }, @@ -1153,17 +1198,17 @@ "source" ], "properties": { - "source": { - "type": "string", - "enum": [ - "path" - ] - }, "position": { "description": "The numeric value specific the location of the argument (starting from 0).", "type": "integer", "format": "uint", "minimum": 0.0 + }, + "source": { + "type": "string", + "enum": [ + "path" + ] } } }, @@ -1176,397 +1221,420 @@ "source" ], "properties": { + "name": { + "description": "The name of the HTTP header.", + "type": "string" + }, "source": { "type": "string", "enum": [ "header" ] - }, - "name": { - "description": "The name of the HTTP header.", - "type": "string" } } } - ] - }, - "JwtAuthPluginConfig": { - "description": "The `jwt_auth` plugin implements the [JSON Web Tokens](https://jwt.io/introduction) specification.\n\nIt can be used to verify the JWT signature, and optionally validate the token issuer and audience. It can also forward the token and its claims to the upstream service.\n\nThe JWKS configuration can be either a local file on the file-system, or a remote JWKS provider.\n\nBy default, the plugin will look for the JWT token in the `Authorization` header, with the `Bearer` prefix.\n\nYou can also configure the plugin to reject requests that don't have a valid JWT token.", - "examples": [ - { - "$metadata": { - "title": "Local JWKS", - "description": "This example is loading a JWKS file from the local file-system. The token is looked up in the `Authorization` header." - }, - "type": "jwt_auth", - "enabled": true, - "config": { - "lookup_locations": [ - { - "source": "header", - "name": "Authorization", - "prefix": "Bearer" - } - ], - "jwks_providers": [ - { - "source": "local", - "path": "jwks.json" - } - ] - } - }, - { - "$metadata": { - "title": "Remote JWKS with prefetch", - "description": "This example is loading a remote JWKS, when the server starts (prefetch). The token is looked up in the `Authorization` header." - }, - "type": "jwt_auth", - "enabled": true, - "config": { - "lookup_locations": [ - { - "source": "header", - "name": "Authorization", - "prefix": "Bearer" - } - ], - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": true - } - ] - } - }, - { - "$metadata": { - "title": "Reject Unauthenticated", - "description": "This example is loading a remote JWKS, and looks for the token in the `auth` cookie. If the token is not present, the request will be rejected." - }, - "type": "jwt_auth", - "enabled": true, - "config": { - "reject_unauthenticated_requests": true, - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": true - } - ], - "lookup_locations": [ - { - "source": "cookies", - "name": "auth" - } - ] - } + ] + }, + "TrustedDocumentsFileFormat": { + "oneOf": [ + { + "title": "apollo_persisted_query_manifest", + "description": "JSON file formated based on [Apollo Persisted Query Manifest](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#1-generate-operation-manifest).", + "type": "string", + "enum": [ + "apollo_persisted_query_manifest" + ] }, + { + "title": "json_key_value", + "description": "A simple JSON map of key-value pairs.\n\nExample: `{\"key1\": \"query { __typename }\"}`", + "type": "string", + "enum": [ + "json_key_value" + ] + } + ] + }, + "TrustedDocumentsPluginConfig": { + "examples": [ { "$metadata": { - "title": "Claims Forwarding", - "description": "This example is loading a remote JWKS, and looks for the token in the `jwt` cookie. If the token is not present, the request will be rejected. The token and its claims will be forwarded to the upstream service in the `X-Auth-Token` and `X-Auth-Claims` headers." + "title": "Local File Store", + "description": "This example is using a local file called `trusted_documents.json` as a store, using the Key->Value map format. The protocol exposed is based on HTTP `POST`, using the `documentId` parameter from the request body." }, - "type": "jwt_auth", + "type": "trusted_documents", "enabled": true, "config": { - "forward_claims_to_upstream_header": "X-Auth-Claims", - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": true - } - ], - "lookup_locations": [ + "protocols": [ { - "source": "cookies", - "name": "jwt" + "type": "document_id", + "field_name": "documentId" } ], - "reject_unauthenticated_requests": true, - "forward_token_to_upstream_header": "X-Auth-Token" + "store": { + "source": "file", + "path": "trusted_documents.json", + "format": "json_key_value" + } } }, { "$metadata": { - "title": "Strict Validation", - "description": "This example is using strict validation, where the token issuer and audience are checked." + "title": "HTTP GET", + "description": "This example uses a local file store called `trusted_documents.json`, using the Key->Value map format. The protocol exposed is based on HTTP `GET`, and extracts all parameters from the query string." }, - "type": "jwt_auth", + "type": "trusted_documents", "enabled": true, "config": { - "lookup_locations": [ - { - "source": "cookies", - "name": "jwt" - } - ], - "jwks_providers": [ + "protocols": [ { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": null + "type": "http_get", + "document_id_from": { + "source": "search_query", + "name": "documentId" + }, + "variables_from": { + "source": "search_query", + "name": "variables" + }, + "operation_name_from": { + "source": "search_query", + "name": "operationName" + } } ], - "issuers": [ - "https://example.com" - ], - "audiences": [ - "realm.myapp.com" - ] + "store": { + "source": "file", + "path": "trusted_documents.json", + "format": "json_key_value" + } } } ], "type": "object", "required": [ - "jwks_providers" + "protocols", + "store" ], "properties": { - "jwks_providers": { - "description": "A list of JWKS providers to use for verifying the JWT signature. Can be either a path to a local JSON of the file-system, or a URL to a remote JWKS provider.", - "type": "array", - "items": { - "$ref": "#/definitions/JwksProviderSourceConfig" - } - }, - "issuers": { - "description": "Specify the [principal](https://tools.ietf.org/html/rfc7519#section-4.1.1) that issued the JWT, usually a URL or an email address. If specified, it has to match the `iss` field in JWT, otherwise the token's `iss` field is not checked.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "audiences": { - "description": "The list of [JWT audiences](https://tools.ietf.org/html/rfc7519#section-4.1.3) are allowed to access. If this field is set, the token's `aud` field must be one of the values in this list, otherwise the token's `aud` field is not checked.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "lookup_locations": { - "description": "A list of locations to look up for the JWT token in the incoming HTTP request. The first one that is found will be used.", - "default": [ - { - "source": "header", - "name": "Authorization", - "prefix": "Bearer" - } - ], - "type": "array", - "items": { - "$ref": "#/definitions/JwtAuthPluginLookupLocation" - } - }, - "reject_unauthenticated_requests": { - "description": "If set to `true`, the entire request will be rejected if the JWT token is not present in the request.", + "allow_untrusted": { + "description": "By default, this plugin does not allow untrusted operations to be executed. This is a security measure to prevent accidental exposure of operations that are not persisted.", "type": [ "boolean", "null" ] }, - "allowed_algorithms": { - "description": "List of allowed algorithms for verifying the JWT signature. If not specified, the default list of all supported algorithms in [`jsonwebtoken` crate](https://crates.io/crates/jsonwebtoken) are used.", - "default": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "EdDSA" - ], - "type": [ - "array", - "null" - ], + "protocols": { + "description": "A list of protocols to be exposed by this plugin. Each protocol defines how to obtain the document ID from the incoming request. You can specify multiple kinds of protocols, if needed.", + "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/TrustedDocumentsProtocolConfig" } }, - "forward_token_to_upstream_header": { - "description": "Forward the JWT token to the upstream service in the specified header.", - "type": [ - "string", - "null" - ] - }, - "forward_claims_to_upstream_header": { - "description": "Forward the JWT claims to the upstream service in the specified header.", - "type": [ - "string", - "null" - ] + "store": { + "description": "The store defines the source of trusted documents. The store contents is a list of hashes and GraphQL documents that are allowed to be executed.", + "$ref": "#/definitions/TrustedDocumentsPluginStoreConfig" } } }, - "JwksProviderSourceConfig": { + "TrustedDocumentsPluginStoreConfig": { "oneOf": [ { - "title": "local", - "description": "A local file on the file-system. This file will be read once on startup and cached.", + "title": "file", + "description": "File-based store configuration. The path specified is relative to the location of the root configuration file. The file contents are loaded into memory on startup. The file is not reloaded automatically. The file format is specified by the `format` field, based on the structure of your file.", "type": "object", "required": [ + "format", "path", "source" ], "properties": { - "source": { - "type": "string", - "enum": [ - "local" - ] + "format": { + "description": "The format and the expected structure of the loaded store file.", + "$ref": "#/definitions/TrustedDocumentsFileFormat" }, "path": { "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", "$ref": "#/definitions/LocalFileReference" - } - } - }, - { - "title": "remote", - "description": "A remote JWKS provider. The JWKS will be fetched via HTTP/HTTPS and cached.", - "type": "object", - "required": [ - "source", - "url" - ], - "properties": { + }, "source": { "type": "string", "enum": [ - "remote" - ] - }, - "url": { - "description": "The URL to fetch the JWKS key set from, via HTTP/HTTPS.", - "type": "string" - }, - "cache_duration": { - "description": "Duration after which the cached JWKS should be expired. If not specified, the default value will be used.", - "default": "10m", - "anyOf": [ - { - "$ref": "#/definitions/Duration" - }, - { - "type": "null" - } - ] - }, - "prefetch": { - "description": "If set to `true`, the JWKS will be fetched on startup and cached. In case of invalid JWKS, the error will be ignored and the plugin will try to fetch again when server receives the first request. If set to `false`, the JWKS will be fetched on-demand, when the first request comes in.", - "type": [ - "boolean", - "null" + "file" ] - } - } - } - ] - }, - "Duration": { - "type": "object", - "required": [ - "nanos", - "secs" - ], - "properties": { - "secs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "nanos": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + } + } } - } + ] }, - "JwtAuthPluginLookupLocation": { + "TrustedDocumentsProtocolConfig": { "oneOf": [ { - "title": "header", + "title": "apollo_manifest_extensions", + "description": "This protocol is based on [Apollo's Persisted Query Extensions](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#2-publish-operation-manifest). The GraphQL operation key is sent over `POST` and contains `extensions` field with the GraphQL document hash.\n\nExample: `POST /graphql {\"extensions\": {\"persistedQuery\": {\"version\": 1, \"sha256Hash\": \"123\"}}`", "type": "object", "required": [ - "name", - "source" + "type" ], "properties": { - "source": { + "type": { "type": "string", "enum": [ - "header" + "apollo_manifest_extensions" ] - }, - "name": { + } + } + }, + { + "title": "document_id", + "description": "This protocol is based on a `POST` request with a JSON body containing a field with the document ID. By default, the field name is `documentId`.\n\nExample: `POST /graphql {\"documentId\": \"123\", \"variables\": {\"code\": \"AF\"}, \"operationName\": \"test\"}`", + "type": "object", + "required": [ + "type" + ], + "properties": { + "field_name": { + "description": "The name of the JSON field containing the document ID in the incoming request.", + "default": "documentId", "type": "string" }, - "prefix": { - "type": [ - "string", - "null" + "type": { + "type": "string", + "enum": [ + "document_id" ] } } }, { - "title": "query_param", + "title": "http_get", + "description": "This protocol is based on a HTTP `GET` request. You can customize where to fetch each one of the parameters from. Each request parameter can be obtained from a different source: query, path, or header. By defualt, all parameters are obtained from the query string.\n\nUnlike other protocols, this protocol does not support sending GraphQL mutations.\n\nExample: `GET /graphql?documentId=123&variables=%7B%22code%22%3A%22AF%22%7D&operationName=test`", "type": "object", "required": [ - "name", - "source" + "type" ], "properties": { - "source": { + "document_id_from": { + "description": "Instructions for fetching the document ID parameter from the incoming HTTP request.", + "default": { + "source": "search_query", + "name": "documentId" + }, + "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + }, + "operation_name_from": { + "description": "Instructions for fetching the operationName parameter from the incoming HTTP request.", + "default": { + "source": "search_query", + "name": "operationName" + }, + "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + }, + "type": { "type": "string", "enum": [ - "query_param" + "http_get" ] }, - "name": { + "variables_from": { + "description": "Instructions for fetching the variables parameter from the incoming HTTP request. GraphQL variables must be passed as a JSON-encoded string.", + "default": { + "source": "search_query", + "name": "variables" + }, + "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + } + } + } + ] + }, + "VrlConfigReference": { + "oneOf": [ + { + "title": "inline", + "description": "Inline string for a VRL code snippet. The string is parsed and executed as a VRL plugin.", + "type": "object", + "required": [ + "content", + "from" + ], + "properties": { + "content": { "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "inline" + ] } } }, { - "title": "cookies", + "title": "file", + "description": "File reference to a VRL file. The file is loaded and executed as a VRL plugin.", "type": "object", "required": [ - "name", - "source" + "from", + "path" ], "properties": { - "source": { + "from": { "type": "string", "enum": [ - "cookies" + "file" ] }, - "name": { - "type": "string" + "path": { + "$ref": "#/definitions/LocalFileReference" } } } ] + }, + "VrlPluginConfig": { + "description": "To simplify the process of extending the functionality of the GraphQL Gateway, we adopted a Rust-based script language called [VRL](https://vector.dev/docs/reference/vrl/).\n\nVRL language is intended for writing simple scripts that can be executed in the context of the GraphQL Gateway. VRL is focused around safety and performance: the script is compiled into Rust code when the server starts, and executed as a native Rust code ([you can find a comparison between VRL and other scripting languages here](https://github.com/YassinEldeeb/rust-embedded-langs-vs-native-benchmark)).\n\n> VRL was initially created to allow users to extend [Vector](https://vector.dev/), a high-performance observability data router, and adopted for Conductor to allow developers to extend the functionality of the GraphQL Gateway easily.\n\n### Writing VRL\n\nVRL is an expression-oriented language. A VRL program consists entirely of expressions, with every expression returning a value. You can define variables, call functions, and use operators to manipulate values.\n\n#### Variables and Functions\n\nThe following program defines a variable `myVar` with the value `\"myValue\"` and prints it to the console:\n\n```vrl\n\nmyVar = \"my value\"\n\nlog(myVar, level:\"info\")\n\n```\n\n#### Assignment\n\nThe `.` is used to set output values. In this example, we are setting the `x-authorization` header of the upstream HTTP request to `my-value`.\n\nHere's an example for a VRL program that extends Conductor's behavior by adding a custom HTTP header to all upstream HTTP requests:\n\n```vrl\n\n.upstream_http_req.headers.\"x-authorization\" = \"my-value\"\n\n```\n\n#### Metadata\n\nThe `%` is used to access metadata values. Note that metadata values are read only.\n\nThe following program is printing a metadata value to the console:\n\n```vrl\n\nlog(%downstream_http_req.headers.authorization, level:\"info\")\n\n```\n\n#### Further Reading\n\n- [VRL Playground](https://playground.vrl.dev/)\n\n- [VRL concepts documentation](https://vector.dev/docs/reference/vrl/#concepts)\n\n- [VRL syntax documentation](https://vector.dev/docs/reference/vrl/expressions/)\n\n- [Compiler errors documentation](https://vector.dev/docs/reference/vrl/errors/)\n\n- [VRL program examples](https://vector.dev/docs/reference/vrl/examples/)\n\n### Runtime Failure Handling\n\nSome VRL functions are fallible, meaning that they can error. Any potential errors thrown by fallible functions must be handled, a requirement enforced at compile time.\n\n```vrl\n\n# This function is fallible, and can create errors, so it must be handled.\n\nparsed, err = parse_json(\"invalid json\")\n\n```\n\nVRL function calls can be marked as infallible by adding a `!` suffix to the function call: (note that this might lead to runtime errors)\n\n```vrl\n\nparsed = parse_json!(\"invalid json\")\n\n```\n\n> In case of a runtime error of a fallible function call, an error will be returned to the end-user, and the gateway will not continue with the execution.\n\n### Input/Output\n\n#### `on_downstream_http_request`\n\nThe `on_downstream_http_request` hook is executed when a downstream HTTP request is received to the gateway from the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_req.body` (type: `string`): The body string of the incoming HTTP request.\n\n- `%downstream_http_req.uri` (type: `string`): The URI of the incoming HTTP request.\n\n- `%downstream_http_req.query_string` (type: `string`): The query string of the incoming HTTP request.\n\n- `%downstream_http_req.method` (type: `string`): The HTTP method of the incoming HTTP request.\n\n- `%downstream_http_req.headers` (type: `object`): The HTTP headers of the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, the gateway will skip the lookup phase, and will use this GraphQL operation instead.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can specify the executable operation by setting this value.\n\n- `.graphql.variables` (type: `object`): The GraphQL variables to be used when executing the GraphQL operation.\n\n- `.graphql.extensions` (type: `object`): The GraphQL extensions to be used when executing the GraphQL operation.\n\n#### `on_downstream_graphql_request`\n\nThe `on_downstream_graphql_request` hook is executed when a GraphQL operation is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_graphql_req.operation` (type: `string`): The GraphQL operation string, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.operation_name`(type: `string`) : If multiple GraphQL operations are set in `%downstream_graphql_req.operation`, you can specify the executable operation by setting this value.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, it will override the existing operation.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can override the extracted value by setting this field.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request. Setting this value will override the existing variables.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request. Setting this value will override the existing extensions.\n\n#### `on_upstream_http_request`\n\nThe `on_upstream_http_request` hook is executed when an HTTP request is about to be sent to the upstream GraphQL server.\n\nThe following metadata inputs are available to the hook:\n\n- `%upstream_http_req.body` (type: `string`): The body string of the planned HTTP request.\n\n- `%upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request.\n\n- `%upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request.\n\n- `%upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request.\n\n- `%upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request.\n\nThe following output values are available to the hook:\n\n- `.upstream_http_req.body` (type: `string`): The body string of the planned HTTP request. Setting this value will override the existing body.\n\n- `.upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request. Setting this value will override the existing URI.\n\n- `.upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request. Setting this value will override the existing query string.\n\n- `.upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request. Setting this value will override the existing HTTP method.\n\n- `.upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n#### `on_downstream_http_response`\n\nThe `on_downstream_http_response` hook is executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_res.body` (type: `string`): The body string of the HTTP response.\n\n- `%downstream_http_res.status` (type: `number`): The status code of the HTTP response.\n\n- `%downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response.\n\nThe following output values are available to the hook:\n\n- `.downstream_http_res.body` (type: `string`): The body string of the HTTP response. Setting this value will override the existing body.\n\n- `.downstream_http_res.status` (type: `number`): The status code of the HTTP response. Setting this value will override the existing status code.\n\n- `.downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n### Shared State\n\nDuring the execution of VRL programs, Conductor configures a shared state object for every incoming HTTP request.\n\nThis means that you can create type-safe shared state objects, and use them to share data between different VRL programs and hooks.\n\nYou can find an example for this in the **Examples** section below.\n\n### Available Functions", + "examples": [ + { + "$metadata": { + "title": "Inline", + "description": "Load and execute VRL plugins using inline configuration." + }, + "type": "vrl", + "enabled": true, + "config": { + "on_upstream_http_request": { + "from": "inline", + "content": ".upstream_http_req.headers.\"x-authorization\" = \"some-value\"\n " + } + } + }, + { + "$metadata": { + "title": "File", + "description": "Load and execute VRL plugins using an external '.vrl' file." + }, + "type": "vrl", + "enabled": true, + "config": { + "on_upstream_http_request": { + "from": "file", + "path": "my_plugin.vrl" + } + } + }, + { + "$metadata": { + "title": "Headers Passthrough", + "description": "This example is using the shared-state feature to store the headers from the incoming HTTP request, and it pass it through to upstream calls." + }, + "type": "vrl", + "enabled": true, + "config": { + "on_upstream_http_request": { + "from": "inline", + "content": ".upstream_http_req.headers = incoming_headers\n " + }, + "on_downstream_http_request": { + "from": "inline", + "content": "incoming_headers = %downstream_http_req.headers\n " + } + } + }, + { + "$metadata": { + "title": "Shared State", + "description": "The following example is configuring a variable, and use it later" + }, + "type": "vrl", + "enabled": true, + "config": { + "on_upstream_http_request": { + "from": "inline", + "content": ".upstream_http_req.headers.\"x-auth\" = authorization_header\n " + }, + "on_downstream_http_request": { + "from": "inline", + "content": "authorization_header = %downstream_http_req.headers.authorization\n " + } + } + }, + { + "$metadata": { + "title": "Short Circuit", + "description": "The following example rejects all incoming requests that doesn't have the \"authorization\" header set." + }, + "type": "vrl", + "enabled": true, + "config": { + "on_downstream_http_request": { + "from": "inline", + "content": "if %downstream_http_req.headers.authorization == null {\nshort_circuit!(403, \"Missing authorization header\")\n}\n " + } + } + }, + { + "$metadata": { + "title": "Custom GraphQL Extraction", + "description": "The following example is using a custom GraphQL extraction, overriding the default gateway behavior. In this example, we parse the incoming body as JSON and use the parsed value to find the GraphQL operation. Assuming the body structure is: `{ \"runThisQuery\": \"query { __typename }\", \"variables\": { }`." + }, + "type": "vrl", + "enabled": true, + "config": { + "on_downstream_http_request": { + "from": "inline", + "content": "parsed_body = parse_json!(%downstream_http_req.body)\n.graphql.operation = parsed_body.runThisQuery\n.graphql.variables = parsed_body.variables\n " + } + } + } + ], + "type": "object", + "properties": { + "on_downstream_graphql_request": { + "description": "A hook executed when a GraphQL query is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent. This hooks allow you to easily manipulate the incoming GraphQL request.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" + }, + { + "type": "null" + } + ] + }, + "on_downstream_http_request": { + "description": "A hook executed when a downstream HTTP request is received to the gateway from the end-user. This hook allow you to extract information from the request, for later use, or to reject a request quickly.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" + }, + { + "type": "null" + } + ] + }, + "on_downstream_http_response": { + "description": "A hook executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user. This hook allow you to manipulate the end-user response easily.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" + }, + { + "type": "null" + } + ] + }, + "on_upstream_http_request": { + "description": "A hook executed when an HTTP request is about to be sent to the upstream GraphQL server. This hook allow you to manipulate upstream HTTP calls easily.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" + }, + { + "type": "null" + } + ] + } + } } } } \ No newline at end of file diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs index 33868d49..34b36eca 100644 --- a/libs/config/src/lib.rs +++ b/libs/config/src/lib.rs @@ -6,7 +6,7 @@ use conductor_common::serde_utils::{ use interpolate::interpolate; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::{fs::read_to_string, path::Path}; +use std::{collections::HashMap, fs::read_to_string, path::Path}; use tracing::{error, warn}; /// This section describes the top-level configuration object for Conductor gateway. @@ -340,13 +340,13 @@ pub enum SourceDefinition { /// The configuration for the GraphQL source. config: GraphQLSourceConfig, }, - #[serde(rename = "mock")] - /// A simple, single GraphQL endpoint - Mock { + #[serde(rename = "federation")] + /// federation endpoint + Federation { /// The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition. id: String, - /// The configuration for the mocked source. - config: MockedSourceConfig, + /// The configuration for the GraphQL source. + config: FederationSourceConfig, }, } @@ -368,8 +368,8 @@ pub struct MockedSourceConfig { fn graphql_source_definition_example() -> JsonSchemaExample { JsonSchemaExample { - metadata: JsonSchemaExampleMetadata::new("Simple", None), wrapper: None, + metadata: JsonSchemaExampleMetadata::new("Simple", None), example: SourceDefinition::GraphQL { id: "my-source".to_string(), config: GraphQLSourceConfig { @@ -379,6 +379,31 @@ fn graphql_source_definition_example() -> JsonSchemaExample { } } +#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)] +pub struct FederationSourceConfig { + /// The endpoint URL for the GraphQL source. + pub supergraph: SupergraphSourceConfig, +} + +#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)] +pub enum SupergraphSourceConfig { + /// The file path for the Supergraph schema. + #[serde(rename = "file")] + File(LocalFileReference), + /// The environment variable that contains the Supergraph schema. + #[serde(rename = "env")] + EnvVar(String), + /// The remote endpoint where the Supergraph schema can be fetched. + #[serde(rename = "remote")] + Remote { + /// The URL endpoint from where to fetch the Supergraph schema. + url: String, + /// Optional headers to include in the request (ex: for authentication) + headers: Option>, + fetch_every: Option, + }, +} + #[tracing::instrument(level = "trace", skip(get_env_value))] pub async fn load_config( file_path: &String, diff --git a/libs/e2e_tests/suite.rs b/libs/e2e_tests/suite.rs index e95c3795..049a12c3 100644 --- a/libs/e2e_tests/suite.rs +++ b/libs/e2e_tests/suite.rs @@ -69,7 +69,7 @@ impl TestSuite { method: Method::POST, query_string: "".to_string(), uri: "/graphql".to_string(), - body: request.to_string().into(), + body: request.operation.into(), headers, }; diff --git a/libs/engine/Cargo.toml b/libs/engine/Cargo.toml index 0cac5272..3ee2d95d 100644 --- a/libs/engine/Cargo.toml +++ b/libs/engine/Cargo.toml @@ -18,6 +18,10 @@ thiserror = { workspace = true } futures = { workspace = true } reqwest = { workspace = true } vrl = { workspace = true } +base64 = { workspace = true } +anyhow = { workspace = true } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { workspace = true } conductor_common = { path = "../common" } conductor_config = { path = "../config" } @@ -30,3 +34,6 @@ http_get_plugin = { path = "../../plugins/http_get" } match_content_type_plugin = { path = "../../plugins/match_content_type" } vrl_plugin = { path = "../../plugins/vrl" } jwt_auth_plugin = { path = "../../plugins/jwt_auth" } +federation_query_planner = { path = "../../libs/federation_query_planner" } +ureq = "2.9.1" +humantime = "2.1.0" diff --git a/libs/engine/src/gateway.rs b/libs/engine/src/gateway.rs index ba8d6e4f..0a05621b 100644 --- a/libs/engine/src/gateway.rs +++ b/libs/engine/src/gateway.rs @@ -12,8 +12,8 @@ use tracing::{debug, error}; use crate::{ plugin_manager::PluginManager, source::{ + federation_source::FederationSourceRuntime, graphql_source::GraphQLSourceRuntime, - mock_source::MockedSourceRuntime, runtime::{SourceError, SourceRuntime}, }, }; @@ -61,8 +61,8 @@ impl ConductorGateway { SourceDefinition::GraphQL { id, config } if id == lookup => { Some(Box::new(GraphQLSourceRuntime::new(config.clone()))) } - SourceDefinition::Mock { id, config } if id == lookup => { - Some(Box::new(MockedSourceRuntime::new(config.clone()))) + SourceDefinition::Federation { id, config } if id == lookup => { + Some(Box::new(FederationSourceRuntime::new(config.clone()))) } _ => None, } diff --git a/libs/engine/src/source/federation_source.rs b/libs/engine/src/source/federation_source.rs new file mode 100644 index 00000000..f3ed9f99 --- /dev/null +++ b/libs/engine/src/source/federation_source.rs @@ -0,0 +1,188 @@ +use super::runtime::{SourceError, SourceRuntime}; +use crate::gateway::ConductorGatewayRouteData; +use base64::{engine, Engine}; +use conductor_common::execute::RequestExecutionContext; +use conductor_common::graphql::GraphQLResponse; +use conductor_config::{FederationSourceConfig, SupergraphSourceConfig}; +use federation_query_planner::execute_federation; +use federation_query_planner::supergraph::{parse_supergraph, Supergraph}; +use std::collections::HashMap; +use std::{future::Future, pin::Pin}; + +#[derive(Debug)] +pub struct FederationSourceRuntime { + pub config: FederationSourceConfig, + pub supergraph: Supergraph, +} + +#[cfg(target_arch = "wasm32")] +pub fn fetch_supergraph_schema( + _url: &str, + _headers: Option<&HashMap>, +) -> Result { + panic!( + "Remote supergraph source not supported in wasm32 at the moment, please fetch it from ENV" + ); +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn fetch_supergraph_schema( + url: &str, + headers: Option<&HashMap>, +) -> Result { + let agent = ureq::Agent::new(); + + let mut request = agent.request("POST", url); + + if let Some(headers_map) = headers { + for (header_name, header_value) in headers_map { + request = request.set(header_name, header_value); + } + } + + match request.call() { + Ok(response) => { + if response.status() == 200 { + match response.into_string() { + Ok(text) => Ok(text), + Err(e) => Err(format!("Failed to read response text: {}", e)), + } + } else { + Err(format!( + "HTTP request failed with status: {}", + response.status() + )) + } + } + Err(e) => Err(format!("HTTP request failed: {}", e)), + } +} + +#[tracing::instrument(level = "trace")] +pub fn load_supergraph( + config: &FederationSourceConfig, // Add the config parameter here +) -> Result> { + match &config.supergraph { + SupergraphSourceConfig::File(file_ref) => { + let content = std::fs::read_to_string(&file_ref.path)?; + Ok(parse_supergraph(&content).unwrap()) + } + SupergraphSourceConfig::EnvVar(env_var) => { + let value = std::env::var(env_var)?; + let decoded = engine::general_purpose::STANDARD_NO_PAD.decode(value)?; + let content = String::from_utf8(decoded)?; + Ok(parse_supergraph(&content).unwrap()) + } + #[cfg(target_arch = "wasm32")] + SupergraphSourceConfig::Remote { + url: _, + headers: _, + fetch_every: _, + } => { + panic!( + "Remote supergraph source not supported in wasm32 at the moment, please fetch it from ENV" + ); + } + #[cfg(not(target_arch = "wasm32"))] + SupergraphSourceConfig::Remote { + url, + headers, + fetch_every, + } => { + // Perform the initial fetch + let supergraph_schema = fetch_supergraph_schema(url, headers.as_ref())?; + let supergraph = parse_supergraph(&supergraph_schema).unwrap(); + + // If `fetch_every` is set, start the periodic fetch + if let Some(interval_str) = fetch_every { + tracing::info!( + "Registered supergraph schema fetch interval to update every: {interval_str}" + ); + let interval = humantime::parse_duration(interval_str)?; + let mut runtime = FederationSourceRuntime { + config: config.clone(), + supergraph: supergraph.clone(), + }; + let url = url.clone(); + let headers = headers.clone(); + tokio::spawn(async move { + runtime.start_periodic_fetch(url, headers, interval).await; + }); + } + + Ok(supergraph) + } + } +} + +impl FederationSourceRuntime { + pub fn new(config: FederationSourceConfig) -> Self { + let supergraph = match load_supergraph(&config) { + Ok(e) => e, + Err(e) => panic!("{e}"), + }; + + Self { config, supergraph } + } + + pub async fn update_supergraph(&mut self, new_schema: String) { + let new_supergraph = parse_supergraph(&new_schema).unwrap(); + self.supergraph = new_supergraph; + } + + #[cfg(not(target_arch = "wasm32"))] + pub async fn start_periodic_fetch( + &mut self, + url: String, + headers: Option>, + interval: std::time::Duration, + ) { + let mut interval_timer = tokio::time::interval(interval); + + loop { + interval_timer.tick().await; + tracing::info!("Fetching new supergraph schema from {url}..."); + match fetch_supergraph_schema(&url, headers.as_ref()) { + Ok(new_schema) => { + self.update_supergraph(new_schema).await; + tracing::info!("Successfully updated supergraph schema after being fetched from {url}"); + } + Err(e) => eprintln!("Failed to fetch supergraph schema: {:?}", e), + } + } + } +} + +impl SourceRuntime for FederationSourceRuntime { + fn execute<'a>( + &'a self, + _route_data: &'a ConductorGatewayRouteData, + request_context: &'a mut RequestExecutionContext, + ) -> Pin> + 'a)>> { + Box::pin(wasm_polyfills::call_async(async move { + let downstream_request = request_context + .downstream_graphql_request + .take() + .expect("GraphQL request isn't available at the time of execution"); + + // let source_req = &mut downstream_request.request; + + // TODO: this needs to be called by conductor execution when fetching subgarphs + // route_data + // .plugin_manager + // .on_upstream_graphql_request(source_req) + // .await; + + let operation = downstream_request.parsed_operation; + + match execute_federation(&self.supergraph, operation).await { + Ok(response_data) => { + let response = serde_json::from_str::(&response_data).unwrap(); + + Ok(response) + } + Err(e) => Err(SourceError::UpstreamPlanningError(e)), + } + })) + } +} diff --git a/libs/engine/src/source/graphql_source.rs b/libs/engine/src/source/graphql_source.rs index c8bd2333..064edb18 100644 --- a/libs/engine/src/source/graphql_source.rs +++ b/libs/engine/src/source/graphql_source.rs @@ -36,10 +36,6 @@ impl GraphQLSourceRuntime { } impl SourceRuntime for GraphQLSourceRuntime { - // #[tracing::instrument( - // skip(self, route_data, request_context), - // name = "GraphQLSourceRuntime::execute" - // )] fn execute<'a>( &'a self, route_data: &'a ConductorGatewayRouteData, diff --git a/libs/engine/src/source/mod.rs b/libs/engine/src/source/mod.rs index c6407490..5cf3aed4 100644 --- a/libs/engine/src/source/mod.rs +++ b/libs/engine/src/source/mod.rs @@ -1,3 +1,4 @@ +pub mod federation_source; pub mod graphql_source; pub mod mock_source; pub mod runtime; diff --git a/libs/engine/src/source/runtime.rs b/libs/engine/src/source/runtime.rs index f930af28..871a4aa3 100644 --- a/libs/engine/src/source/runtime.rs +++ b/libs/engine/src/source/runtime.rs @@ -22,6 +22,8 @@ pub enum SourceError { ShortCircuit, #[error("network error: {0}")] NetworkError(reqwest::Error), + #[error("upstream planning error: {0}")] + UpstreamPlanningError(anyhow::Error), } impl From for GraphQLResponse { diff --git a/libs/federation_query_planner/.gitignore b/libs/federation_query_planner/.gitignore new file mode 100644 index 00000000..5feb9078 --- /dev/null +++ b/libs/federation_query_planner/.gitignore @@ -0,0 +1,2 @@ +target +node_modules \ No newline at end of file diff --git a/libs/federation_query_planner/Cargo.lock b/libs/federation_query_planner/Cargo.lock new file mode 100644 index 00000000..7c90cfb4 --- /dev/null +++ b/libs/federation_query_planner/Cargo.lock @@ -0,0 +1,2201 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "async-graphql" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0def00150a38be3267a3796b9c7feda20262dc879f9ae9d48464b24813da2b69" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64 0.13.1", + "bytes", + "fast_chemail", + "fnv", + "futures-util", + "handlebars", + "http", + "indexmap 2.0.0", + "mime", + "multer", + "num-traits", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions", + "tempfile", + "thiserror", +] + +[[package]] +name = "async-graphql-derive" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aea94ff9eaaf80d13e862754192de44c5d93eb660c6688bd0f43f1c441f67b8" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum", + "syn", + "thiserror", +] + +[[package]] +name = "async-graphql-parser" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b898be948c43e00babf9154f5e92d12d011698f0171e2da9d2cfc2ffcfeaf28f" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afef4917e23f7e651074dbfca64d82f194b817f00bd74d9df05d5408eb83e1e" +dependencies = [ + "bytes", + "indexmap 2.0.0", + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "graphql-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" +dependencies = [ + "combine", + "thiserror", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "handlebars" +version = "4.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c3372087601b532857d332f5957cbae686da52bb7810bf038c3e3c3cc2fa0d" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", + "serde", +] + +[[package]] +name = "insta" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0770b0a3d4c70567f0d58331f3088b0e4c4f56c9b8d764efe654b4a5d46de3a" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "yaml-rust", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "openssl" +version = "0.10.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f94bca7e7a599d89dea5dfa309e217e7906c3c007fb9c3299c40b10d6a315d3" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d490fe7e8556575ff6911e45567ab95e71617f43781e5c05490dc8d75c965c" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "query_planner" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-graphql", + "async-trait", + "criterion", + "futures", + "graphql-parser", + "insta", + "lazy_static", + "linked-hash-map", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix 0.37.23", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.3", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/libs/federation_query_planner/Cargo.toml b/libs/federation_query_planner/Cargo.toml new file mode 100644 index 00000000..118f8f50 --- /dev/null +++ b/libs/federation_query_planner/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "federation_query_planner" +version = "0.1.0" +edition = "2021" + +[lib] +bench = false + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +anyhow = { workspace = true } +graphql-parser = "0.4.0" +reqwest = { workspace = true, features = ["json"] } +linked-hash-map = "0.5.6" +futures = { workspace = true } +lazy_static = "1.4.0" +tracing = { workspace = true } +async-graphql = {version = "6.0.11", features = ["dynamic-schema"]} + +[dev-dependencies] +insta = { version = "1.29.0", features = ["yaml", "json"] } +criterion = { version = "0.5.1", features = ["html_reports"] } +tokio = { version = "1.35.0", features = ["full"] } diff --git a/libs/federation_query_planner/benches/bench.rs b/libs/federation_query_planner/benches/bench.rs new file mode 100644 index 00000000..a41a2693 --- /dev/null +++ b/libs/federation_query_planner/benches/bench.rs @@ -0,0 +1,50 @@ +use std::fs; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use federation_query_planner::{ + query_planner::plan_for_user_query, supergraph::parse_supergraph, user_query::parse_user_query, +}; + +const SAMPLE_SIZE: usize = 100; + +fn criterion_benchmark(c: &mut Criterion) { + let supergraph = fs::read_to_string("./benches/fixtures/complex-supergraph.graphql").unwrap(); + let query = fs::read_to_string("./benches/fixtures/huge-query.graphql").unwrap(); + + // c.bench_function("Parsing User Query", |b| { + // b.iter(|| { + // let user_query = parse_user_query(&query); + + // black_box(user_query) + // }) + // }); + // c.bench_function("Parsing Supergraph Schema", |b| { + // b.iter(|| { + // let parsed_supergraph = parse_supergraph(&supergraph); + + // black_box(parsed_supergraph) + // }) + // }); + + let parsed_supergraph = parse_supergraph(&supergraph).unwrap(); + let mut user_query = parse_user_query(&query); + + c.bench_function("Construct Query Plan", |b| { + b.iter(|| { + let plan = plan_for_user_query(&parsed_supergraph, &mut user_query); + + black_box(plan) + }) + }); +} + +fn configure_benchmark() -> Criterion { + Criterion::default().sample_size(SAMPLE_SIZE) +} + +criterion_group! { + name = benches; + config = configure_benchmark(); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/libs/federation_query_planner/benches/fixtures/complex-supergraph.graphql b/libs/federation_query_planner/benches/fixtures/complex-supergraph.graphql new file mode 100644 index 00000000..e8aa3823 --- /dev/null +++ b/libs/federation_query_planner/benches/fixtures/complex-supergraph.graphql @@ -0,0 +1,79 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.2") + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) { + query: Query +} + +directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + provides: join__FieldSet + requires: join__FieldSet +) on FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT + +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT + +type Product + @join__owner(graph: SERVICE1) + @join__type(graph: SERVICE1, key: "upc") + @join__type(graph: SERVICE2, key: "upc") + @join__type(graph: SERVICE3, key: "upc") { + inStock: Boolean @join__field(graph: SERVICE3) + name: String @join__field(graph: SERVICE1) + price: Int @join__field(graph: SERVICE1) + reviews: [Review] @join__field(graph: SERVICE2) + shippingEstimate: Int @join__field(graph: SERVICE3, requires: "price weight") + upc: String! @join__field(graph: SERVICE1) + weight: Int @join__field(graph: SERVICE1) +} + +type Query { + me: User @join__field(graph: SERVICE0) + topProducts(first: Int): [Product] @join__field(graph: SERVICE1) + users: [User] @join__field(graph: SERVICE0) +} + +type Review @join__owner(graph: SERVICE2) @join__type(graph: SERVICE2, key: "id") { + author: User @join__field(graph: SERVICE2, provides: "username") + body: String @join__field(graph: SERVICE2) + id: ID! @join__field(graph: SERVICE2) + product: Product @join__field(graph: SERVICE2) +} + +type User + @join__owner(graph: SERVICE0) + @join__type(graph: SERVICE0, key: "id") + @join__type(graph: SERVICE2, key: "id") { + birthDate: String @join__field(graph: SERVICE0) + id: ID! @join__field(graph: SERVICE0) + name: String @join__field(graph: SERVICE0) + numberOfReviews: Int @join__field(graph: SERVICE2) + reviews: [Review] @join__field(graph: SERVICE2) + username: String @join__field(graph: SERVICE0) +} + +enum core__Purpose { + """ + `EXECUTION` features provide metadata necessary to for operation execution. + """ + EXECUTION + + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY +} + +scalar join__FieldSet + +enum join__Graph { + SERVICE0 @join__graph(name: "service0", url: "http://www.service-0.com") + SERVICE1 @join__graph(name: "service1", url: "http://www.service-1.com") + SERVICE2 @join__graph(name: "service2", url: "http://www.service-2.com") + SERVICE3 @join__graph(name: "service3", url: "http://www.service-3.com") +} \ No newline at end of file diff --git a/libs/federation_query_planner/benches/fixtures/huge-query.graphql b/libs/federation_query_planner/benches/fixtures/huge-query.graphql new file mode 100644 index 00000000..daa287ad --- /dev/null +++ b/libs/federation_query_planner/benches/fixtures/huge-query.graphql @@ -0,0 +1,2698 @@ +fragment User on User { + id + username + name + ...Review +} + +fragment Review on Review { + id + body +} + +fragment Product on Product { + inStock + name + price + shippingEstimate + upc + weight +} + +query HowYouDoing { + locations0 { + name0 + description0 + reviews0 { + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + comment0 + rating0 + } + isCool0 { + really0 { + yes0 + } + } + } + + location0(id: "country0") { + photo0 + } + + locations1 { + name1 + description1 + reviews1 { + comment1 + rating1 + } + isCool1 { + really1 { + yes1 + } + } + } + + location1(id: "country1") { + photo1 + } + + locations2 { + name2 + description2 + reviews2 { + comment2 + rating2 + } + isCool2 { + really2 { + yes2 + } + } + } + + location2(id: "country2") { + photo2 + } + + locations3 { + name3 + description3 + reviews3 { + comment3 + rating3 + } + isCool3 { + really3 { + yes3 + } + } + } + + location3(id: "country3") { + photo3 + } + + locations4 { + name4 + description4 + reviews4 { + comment4 + rating4 + } + isCool4 { + really4 { + yes4 + } + } + } + + location4(id: "country4") { + photo4 + } + + locations5 { + name5 + description5 + reviews5 { + comment5 + rating5 + } + isCool5 { + really5 { + yes5 + } + } + } + + location5(id: "country5") { + photo5 + } + + locations6 { + name6 + description6 + reviews6 { + comment6 + rating6 + } + isCool6 { + really6 { + yes6 + } + } + } + + location6(id: "country6") { + photo6 + } + + locations7 { + name7 + description7 + reviews7 { + comment7 + rating7 + } + isCool7 { + really7 { + yes7 + } + } + } + + location7(id: "country7") { + photo7 + } + + locations8 { + name8 + description8 + reviews8 { + comment8 + rating8 + } + isCool8 { + really8 { + yes8 + } + } + } + + location8(id: "country8") { + photo8 + } + + locations9 { + name9 + description9 + reviews9 { + comment9 + rating9 + } + isCool9 { + really9 { + yes9 + } + } + } + + location9(id: "country9") { + photo9 + } + + locations10 { + name10 + description10 + reviews10 { + comment10 + rating10 + } + isCool10 { + really10 { + yes10 + } + } + } + + location10(id: "country10") { + photo10 + } + + locations11 { + name11 + description11 + reviews11 { + comment11 + rating11 + } + isCool11 { + really11 { + yes11 + } + } + } + + location11(id: "country11") { + photo11 + } + + locations12 { + name12 + description12 + reviews12 { + comment12 + rating12 + } + isCool12 { + really12 { + yes12 + } + } + } + + location12(id: "country12") { + photo12 + } + + locations13 { + name13 + description13 + reviews13 { + comment13 + rating13 + } + isCool13 { + really13 { + yes13 + } + } + } + + location13(id: "country13") { + photo13 + } + + locations14 { + name14 + description14 + reviews14 { + comment14 + rating14 + } + isCool14 { + really14 { + yes14 + } + } + } + + location14(id: "country14") { + photo14 + } + + locations15 { + name15 + description15 + reviews15 { + comment15 + rating15 + } + isCool15 { + really15 { + yes15 + } + } + } + + location15(id: "country15") { + photo15 + } + + locations16 { + name16 + description16 + reviews16 { + comment16 + rating16 + } + isCool16 { + really16 { + yes16 + } + } + } + + location16(id: "country16") { + photo16 + } + + locations17 { + name17 + description17 + reviews17 { + comment17 + rating17 + } + isCool17 { + really17 { + yes17 + } + } + } + + location17(id: "country17") { + photo17 + } + + locations18 { + name18 + description18 + reviews18 { + comment18 + rating18 + } + isCool18 { + really18 { + yes18 + } + } + } + + location18(id: "country18") { + photo18 + } + + locations19 { + name19 + description19 + reviews19 { + comment19 + rating19 + } + isCool19 { + really19 { + yes19 + } + } + } + + location19(id: "country19") { + photo19 + } + + locations20 { + name20 + description20 + reviews20 { + comment20 + rating20 + } + isCool20 { + really20 { + yes20 + } + } + } + + location20(id: "country20") { + photo20 + } + + locations21 { + name21 + description21 + reviews21 { + comment21 + rating21 + } + isCool21 { + really21 { + yes21 + } + } + } + + location21(id: "country21") { + photo21 + } + + locations22 { + name22 + description22 + reviews22 { + comment22 + rating22 + } + isCool22 { + really22 { + yes22 + } + } + } + + location22(id: "country22") { + photo22 + } + + locations23 { + name23 + description23 + reviews23 { + comment23 + rating23 + } + isCool23 { + really23 { + yes23 + } + } + } + + location23(id: "country23") { + photo23 + } + + locations24 { + name24 + description24 + reviews24 { + comment24 + rating24 + } + isCool24 { + really24 { + yes24 + } + } + } + + location24(id: "country24") { + photo24 + } + + locations25 { + name25 + description25 + reviews25 { + comment25 + rating25 + } + isCool25 { + really25 { + yes25 + } + } + } + + location25(id: "country25") { + photo25 + } + + locations26 { + name26 + description26 + reviews26 { + comment26 + rating26 + } + isCool26 { + really26 { + yes26 + } + } + } + + location26(id: "country26") { + photo26 + } + + locations27 { + name27 + description27 + reviews27 { + comment27 + rating27 + } + isCool27 { + really27 { + yes27 + } + } + } + + location27(id: "country27") { + photo27 + } + + locations28 { + name28 + description28 + reviews28 { + comment28 + rating28 + } + isCool28 { + really28 { + yes28 + } + } + } + + location28(id: "country28") { + photo28 + } + + locations29 { + name29 + description29 + reviews29 { + comment29 + rating29 + } + isCool29 { + really29 { + yes29 + } + } + } + + location29(id: "country29") { + photo29 + } + + locations30 { + name30 + description30 + reviews30 { + comment30 + rating30 + } + isCool30 { + really30 { + yes30 + } + } + } + + location30(id: "country30") { + photo30 + } + + locations31 { + name31 + description31 + reviews31 { + comment31 + rating31 + } + isCool31 { + really31 { + yes31 + } + } + } + + location31(id: "country31") { + photo31 + } + + locations32 { + name32 + description32 + reviews32 { + comment32 + rating32 + } + isCool32 { + really32 { + yes32 + } + } + } + + location32(id: "country32") { + photo32 + } + + locations33 { + name33 + description33 + reviews33 { + comment33 + rating33 + } + isCool33 { + really33 { + yes33 + } + } + } + + location33(id: "country33") { + photo33 + } + + locations34 { + name34 + description34 + reviews34 { + comment34 + rating34 + } + isCool34 { + really34 { + yes34 + } + } + } + + location34(id: "country34") { + photo34 + } + + locations35 { + name35 + description35 + reviews35 { + comment35 + rating35 + } + isCool35 { + really35 { + yes35 + } + } + } + + location35(id: "country35") { + photo35 + } + + locations36 { + name36 + description36 + reviews36 { + comment36 + rating36 + } + isCool36 { + really36 { + yes36 + } + } + } + + location36(id: "country36") { + photo36 + } + + locations37 { + name37 + description37 + reviews37 { + comment37 + rating37 + } + isCool37 { + really37 { + yes37 + } + } + } + + location37(id: "country37") { + photo37 + } + + locations38 { + name38 + description38 + reviews38 { + comment38 + rating38 + } + isCool38 { + really38 { + yes38 + } + } + } + + location38(id: "country38") { + photo38 + } + + locations39 { + name39 + description39 + reviews39 { + comment39 + rating39 + } + isCool39 { + really39 { + yes39 + } + } + } + + location39(id: "country39") { + photo39 + } + + locations40 { + name40 + description40 + reviews40 { + comment40 + rating40 + } + isCool40 { + really40 { + yes40 + } + } + } + + location40(id: "country40") { + photo40 + } + + locations41 { + name41 + description41 + reviews41 { + comment41 + rating41 + } + isCool41 { + really41 { + yes41 + } + } + } + + location41(id: "country41") { + photo41 + } + + locations42 { + name42 + description42 + reviews42 { + comment42 + rating42 + } + isCool42 { + really42 { + yes42 + } + } + } + + location42(id: "country42") { + photo42 + } + + locations43 { + name43 + description43 + reviews43 { + comment43 + rating43 + } + isCool43 { + really43 { + yes43 + } + } + } + + location43(id: "country43") { + photo43 + } + + locations44 { + name44 + description44 + reviews44 { + comment44 + rating44 + } + isCool44 { + really44 { + yes44 + } + } + } + + location44(id: "country44") { + photo44 + } + + locations45 { + name45 + description45 + reviews45 { + comment45 + rating45 + } + isCool45 { + really45 { + yes45 + } + } + } + + location45(id: "country45") { + photo45 + } + + locations46 { + name46 + description46 + reviews46 { + comment46 + rating46 + } + isCool46 { + really46 { + yes46 + } + } + } + + location46(id: "country46") { + photo46 + } + + locations47 { + name47 + description47 + reviews47 { + comment47 + rating47 + } + isCool47 { + really47 { + yes47 + } + } + } + + location47(id: "country47") { + photo47 + } + + locations48 { + name48 + description48 + reviews48 { + comment48 + rating48 + } + isCool48 { + really48 { + yes48 + } + } + } + + location48(id: "country48") { + photo48 + } + + locations49 { + name49 + description49 + reviews49 { + comment49 + rating49 + } + isCool49 { + really49 { + yes49 + } + } + } + + location49(id: "country49") { + photo49 + } + + locations50 { + name50 + description50 + reviews50 { + comment50 + rating50 + } + isCool50 { + really50 { + yes50 + } + } + } + + location50(id: "country50") { + photo50 + } + + locations51 { + name51 + description51 + reviews51 { + comment51 + rating51 + } + isCool51 { + really51 { + yes51 + } + } + } + + location51(id: "country51") { + photo51 + } + + locations52 { + name52 + description52 + reviews52 { + comment52 + rating52 + } + isCool52 { + really52 { + yes52 + } + } + } + + location52(id: "country52") { + photo52 + } + + locations53 { + name53 + description53 + reviews53 { + comment53 + rating53 + } + isCool53 { + really53 { + yes53 + } + } + } + + location53(id: "country53") { + photo53 + } + + locations54 { + name54 + description54 + reviews54 { + comment54 + rating54 + } + isCool54 { + really54 { + yes54 + } + } + } + + location54(id: "country54") { + photo54 + } + + locations55 { + name55 + description55 + reviews55 { + comment55 + rating55 + } + isCool55 { + really55 { + yes55 + } + } + } + + location55(id: "country55") { + photo55 + } + + locations56 { + name56 + description56 + reviews56 { + comment56 + rating56 + } + isCool56 { + really56 { + yes56 + } + } + } + + location56(id: "country56") { + photo56 + } + + locations57 { + name57 + description57 + reviews57 { + comment57 + rating57 + } + isCool57 { + really57 { + yes57 + } + } + } + + location57(id: "country57") { + photo57 + } + + locations58 { + name58 + description58 + reviews58 { + comment58 + rating58 + } + isCool58 { + really58 { + yes58 + } + } + } + + location58(id: "country58") { + photo58 + } + + locations59 { + name59 + description59 + reviews59 { + comment59 + rating59 + } + isCool59 { + really59 { + yes59 + } + } + } + + location59(id: "country59") { + photo59 + } + + locations60 { + name60 + description60 + reviews60 { + comment60 + rating60 + } + isCool60 { + really60 { + yes60 + } + } + } + + location60(id: "country60") { + photo60 + } + + locations61 { + name61 + description61 + reviews61 { + comment61 + rating61 + } + isCool61 { + really61 { + yes61 + } + } + } + + location61(id: "country61") { + photo61 + } + + locations62 { + name62 + description62 + reviews62 { + comment62 + rating62 + } + isCool62 { + really62 { + yes62 + } + } + } + + location62(id: "country62") { + photo62 + } + + locations63 { + name63 + description63 + reviews63 { + comment63 + rating63 + } + isCool63 { + really63 { + yes63 + } + } + } + + location63(id: "country63") { + photo63 + } + + locations64 { + name64 + description64 + reviews64 { + comment64 + rating64 + } + isCool64 { + really64 { + yes64 + } + } + } + + location64(id: "country64") { + photo64 + } + + locations65 { + name65 + description65 + reviews65 { + comment65 + rating65 + } + isCool65 { + really65 { + yes65 + } + } + } + + location65(id: "country65") { + photo65 + } + + locations66 { + name66 + description66 + reviews66 { + comment66 + rating66 + } + isCool66 { + really66 { + yes66 + } + } + } + + location66(id: "country66") { + photo66 + } + + locations67 { + name67 + description67 + reviews67 { + comment67 + rating67 + } + isCool67 { + really67 { + yes67 + } + } + } + + location67(id: "country67") { + photo67 + } + + locations68 { + name68 + description68 + reviews68 { + comment68 + rating68 + } + isCool68 { + really68 { + yes68 + } + } + } + + location68(id: "country68") { + photo68 + } + + locations69 { + name69 + description69 + reviews69 { + comment69 + rating69 + } + isCool69 { + really69 { + yes69 + } + } + } + + location69(id: "country69") { + photo69 + } + + locations70 { + name70 + description70 + reviews70 { + comment70 + rating70 + } + isCool70 { + really70 { + yes70 + } + } + } + + location70(id: "country70") { + photo70 + } + + locations71 { + name71 + description71 + reviews71 { + comment71 + rating71 + } + isCool71 { + really71 { + yes71 + } + } + } + + location71(id: "country71") { + photo71 + } + + locations72 { + name72 + description72 + reviews72 { + comment72 + rating72 + } + isCool72 { + really72 { + yes72 + } + } + } + + location72(id: "country72") { + photo72 + } + + locations73 { + name73 + description73 + reviews73 { + comment73 + rating73 + } + isCool73 { + really73 { + yes73 + } + } + } + + location73(id: "country73") { + photo73 + } + + locations74 { + name74 + description74 + reviews74 { + comment74 + rating74 + } + isCool74 { + really74 { + yes74 + } + } + } + + location74(id: "country74") { + photo74 + } + + locations75 { + name75 + description75 + reviews75 { + comment75 + rating75 + } + isCool75 { + really75 { + yes75 + } + } + } + + location75(id: "country75") { + photo75 + } + + locations76 { + name76 + description76 + reviews76 { + comment76 + rating76 + } + isCool76 { + really76 { + yes76 + } + } + } + + location76(id: "country76") { + photo76 + } + + locations77 { + name77 + description77 + reviews77 { + comment77 + rating77 + } + isCool77 { + really77 { + yes77 + } + } + } + + location77(id: "country77") { + photo77 + } + + locations78 { + name78 + description78 + reviews78 { + comment78 + rating78 + } + isCool78 { + really78 { + yes78 + } + } + } + + location78(id: "country78") { + photo78 + } + + locations79 { + name79 + description79 + reviews79 { + comment79 + rating79 + } + isCool79 { + really79 { + yes79 + } + } + } + + location79(id: "country79") { + photo79 + } + + locations80 { + name80 + description80 + reviews80 { + comment80 + rating80 + } + isCool80 { + really80 { + yes80 + } + } + } + + location80(id: "country80") { + photo80 + } + + locations81 { + name81 + description81 + reviews81 { + comment81 + rating81 + } + isCool81 { + really81 { + yes81 + } + } + } + + location81(id: "country81") { + photo81 + } + + locations82 { + name82 + description82 + reviews82 { + comment82 + rating82 + } + isCool82 { + really82 { + yes82 + } + } + } + + location82(id: "country82") { + photo82 + } + + locations83 { + name83 + description83 + reviews83 { + comment83 + rating83 + } + isCool83 { + really83 { + yes83 + } + } + } + + location83(id: "country83") { + photo83 + } + + locations84 { + name84 + description84 + reviews84 { + comment84 + rating84 + } + isCool84 { + really84 { + yes84 + } + } + } + + location84(id: "country84") { + photo84 + } + + locations85 { + name85 + description85 + reviews85 { + comment85 + rating85 + } + isCool85 { + really85 { + yes85 + } + } + } + + location85(id: "country85") { + photo85 + } + + locations86 { + name86 + description86 + reviews86 { + comment86 + rating86 + } + isCool86 { + really86 { + yes86 + } + } + } + + location86(id: "country86") { + photo86 + } + + locations87 { + name87 + description87 + reviews87 { + comment87 + rating87 + } + isCool87 { + really87 { + yes87 + } + } + } + + location87(id: "country87") { + photo87 + } + + locations88 { + name88 + description88 + reviews88 { + comment88 + rating88 + } + isCool88 { + really88 { + yes88 + } + } + } + + location88(id: "country88") { + photo88 + } + + locations89 { + name89 + description89 + reviews89 { + comment89 + rating89 + } + isCool89 { + really89 { + yes89 + } + } + } + + location89(id: "country89") { + photo89 + } + + locations90 { + name90 + description90 + reviews90 { + comment90 + rating90 + } + isCool90 { + really90 { + yes90 + } + } + } + + location90(id: "country90") { + photo90 + } + + locations91 { + name91 + description91 + reviews91 { + comment91 + rating91 + } + isCool91 { + really91 { + yes91 + } + } + } + + location91(id: "country91") { + photo91 + } + + locations92 { + name92 + description92 + reviews92 { + comment92 + rating92 + } + isCool92 { + really92 { + yes92 + } + } + } + + location92(id: "country92") { + photo92 + } + + locations93 { + name93 + description93 + reviews93 { + comment93 + rating93 + } + isCool93 { + really93 { + yes93 + } + } + } + + location93(id: "country93") { + photo93 + } + + locations94 { + name94 + description94 + reviews94 { + comment94 + rating94 + } + isCool94 { + really94 { + yes94 + } + } + } + + location94(id: "country94") { + photo94 + } + + locations95 { + name95 + description95 + reviews95 { + comment95 + rating95 + } + isCool95 { + really95 { + yes95 + } + } + } + + location95(id: "country95") { + photo95 + } + + locations96 { + name96 + description96 + reviews96 { + comment96 + rating96 + } + isCool96 { + really96 { + yes96 + } + } + } + + location96(id: "country96") { + photo96 + } + + locations97 { + name97 + description97 + reviews97 { + comment97 + rating97 + } + isCool97 { + really97 { + yes97 + } + } + } + + location97(id: "country97") { + photo97 + } + + locations98 { + name98 + description98 + reviews98 { + comment98 + rating98 + } + isCool98 { + really98 { + yes98 + } + } + } + + location98(id: "country98") { + photo98 + } + + locations99 { + name99 + description99 + reviews99 { + comment99 + rating99 + } + isCool99 { + really99 { + yes99 + } + } + } + + location99(id: "country99") { + photo99 + } +} diff --git a/libs/federation_query_planner/src/constants.rs b/libs/federation_query_planner/src/constants.rs new file mode 100644 index 00000000..8eca4173 --- /dev/null +++ b/libs/federation_query_planner/src/constants.rs @@ -0,0 +1 @@ +pub const CONDUCTOR_INTERNAL_SERVICE_RESOLVER: &str = "$CONDUCTOR$INTERNAL$INTROSPECTION$HANDLER$"; diff --git a/libs/federation_query_planner/src/executor.rs b/libs/federation_query_planner/src/executor.rs new file mode 100644 index 00000000..eba5e9b8 --- /dev/null +++ b/libs/federation_query_planner/src/executor.rs @@ -0,0 +1,276 @@ +use std::pin::Pin; + +use crate::constants::CONDUCTOR_INTERNAL_SERVICE_RESOLVER; +use crate::query_planner::Parallel; + +use super::query_planner::{QueryPlan, QueryStep}; +use super::supergraph::Supergraph; +use anyhow::{anyhow, Result}; +use async_graphql::{dynamic::*, Error, Value}; +use futures::future::join_all; +use futures::Future; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use serde_json::Value as SerdeValue; + +lazy_static! { + static ref CLIENT: reqwest::Client = reqwest::Client::new(); +} + +pub async fn execute_query_plan( + query_plan: &QueryPlan, + supergraph: &Supergraph, +) -> Result>> { + let mut all_futures = Vec::new(); + + for step in &query_plan.parallel_steps { + match step { + Parallel::Sequential(query_steps) => { + let future = execute_sequential(query_steps, supergraph); + all_futures.push(future); + } + } + } + + let results: Result, _> = join_all(all_futures).await.into_iter().collect(); + + match results { + Ok(val) => Ok(val), + Err(e) => Err(anyhow!(e)), + } +} + +async fn execute_sequential( + query_steps: &Vec, + supergraph: &Supergraph, +) -> Result> { + let mut data_vec = vec![]; + let mut entity_arguments: Option = None; + + for (i, query_step) in query_steps.iter().enumerate() { + let data = execute_query_step(query_step, supergraph, entity_arguments.clone()).await; + + match data { + Ok(data) => { + data_vec.push(( + (query_step.service_name.clone(), query_step.query.clone()), + data, + )); + + if i + 1 < query_steps.len() { + let next_step = &query_steps[i + 1]; + match &next_step.entity_query_needs { + Some(needs) => { + data_vec.iter().find(|&data| { + if let Some(x) = data.1.data.as_ref() { + // recursively search and find match + let y = find_objects_matching_criteria( + x, + &needs.__typename, + &needs.fields.clone().into_iter().next().unwrap(), + ); + + if y.is_empty() { + return false; + } else { + entity_arguments = Some(SerdeValue::from(y)); + return true; + } + } + + false + }); + + Some(serde_json::json!({ "representations": entity_arguments })) + } + None => None, + } + } else { + None + }; + } + Err(err) => return Err(err), + } + } + + let x: Vec<((String, String), QueryResponse)> = data_vec + .into_iter() + .map(|(plan_meta, response)| { + let new_response = QueryResponse { + data: response.data, + // Initialize other fields of QueryResponse as needed + errors: response.errors, + extensions: None, + }; + (plan_meta, new_response) + }) + .collect::>(); + + Ok(x) +} + +fn find_objects_matching_criteria( + json: &SerdeValue, + typename: &str, + field: &str, +) -> Vec { + let mut matching_objects = Vec::new(); + + match json { + SerdeValue::Object(map) => { + if let (Some(typename_value), Some(field_value)) = ( + map.get("__typename").and_then(|v| v.as_str()), + map.get(field), + ) { + if typename_value == typename { + let mut result = serde_json::Map::new(); + result.insert( + "__typename".to_string(), + SerdeValue::String(typename.to_string()), + ); + result.insert(field.to_string(), field_value.clone()); + matching_objects.push(SerdeValue::Object(result)); + } + } + for (_, value) in map { + matching_objects.extend(find_objects_matching_criteria(value, typename, field)); + } + } + SerdeValue::Array(arr) => { + for element in arr { + matching_objects.extend(find_objects_matching_criteria(element, typename, field)); + } + } + _ => {} + } + + matching_objects +} + +#[derive(Deserialize, Debug, Serialize, Default)] +pub struct QueryResponse { + pub data: Option, + pub errors: Option>, + pub extensions: Option, +} + +fn dynamically_build_schema_from_supergraph(supergraph: &Supergraph) -> Schema { + let mut query = Object::new("Query"); + + // Dynamically create object types and fields + for (type_name, graphql_type) in &supergraph.types { + let mut obj = Object::new(type_name); + + for field_name in graphql_type.fields.keys() { + let field_type = TypeRef::named_nn(TypeRef::STRING); // Adjust based on `field.field_type` + obj = obj.field(Field::new(field_name, field_type, move |_| { + let future: Pin, Error>> + Send>> = + Box::pin(async move { + Ok(Some(FieldValue::from(Value::String( + "Dynamic value".to_string(), + )))) + }); + FieldFuture::new(future) + })); + } + + // Adjust the creation of Object TypeRef + // Placeholder logic - replace with the correct object creation + let obj_type_ref = TypeRef::named(TypeRef::STRING); // This needs to be correctly set + query = query.field(Field::new(type_name, obj_type_ref, move |_| { + let future: Pin, Error>> + Send>> = + Box::pin(async move { Ok(Some(FieldValue::from(Value::Object(Default::default())))) }); + FieldFuture::new(future) + })); + } + + // Construct and return the schema + + Schema::build("Query", None, None) + .register(query) + .finish() + .expect("Introspection schema build failed") +} + +async fn execute_query_step( + query_step: &QueryStep, + supergraph: &Supergraph, + entity_arguments: Option, +) -> Result { + let is_introspection = query_step.service_name == CONDUCTOR_INTERNAL_SERVICE_RESOLVER; + + if is_introspection { + let schema = dynamically_build_schema_from_supergraph(supergraph); + + // Execute the introspection query + // TODO: whenever excuting a query step, we need to take the query out of the step's struct instead of copying it + let request = async_graphql::Request::new(query_step.query.to_string()); + let response = schema.execute(request).await; + + let data = serde_json::to_value(response.data)?; + let errors = response + .errors + .iter() + .map(|e| serde_json::to_value(e).unwrap()) + .collect(); + + Ok(QueryResponse { + data: Some(data), + errors: Some(errors), + extensions: None, + }) + } else { + let url = supergraph.subgraphs.get(&query_step.service_name).unwrap(); + + let variables_object = if let Some(arguments) = &entity_arguments { + serde_json::json!({ "representations": arguments }) + } else { + SerdeValue::Object(serde_json::Map::new()) + }; + + let response = match CLIENT + .post(url) + .header("Content-Type", "application/json") + .body( + serde_json::json!({ + "query": query_step.query, + "variables": variables_object + }) + .to_string(), + ) + .send() + .await + { + Ok(resp) => resp, + Err(err) => { + eprintln!("Failed to send request: {}", err); + return Err(anyhow::anyhow!("Failed to send request: {}", err)); + } + }; + + if !response.status().is_success() { + eprintln!("Received error response: {:?}", response.status()); + return Err(anyhow::anyhow!( + "Failed request with status: {}", + response.status() + )); + } + + let response_data = match response.json::().await { + Ok(data) => data, + Err(err) => { + eprintln!("Failed to parse response: {}", err); + return Err(anyhow::anyhow!("Failed to parse response: {}", err)); + } + }; + + // Check if there were any GraphQL errors + if let Some(errors) = &response_data.errors { + for error in errors { + eprintln!("Error: {:?}", error); + } + } + + Ok(response_data) + } +} diff --git a/libs/federation_query_planner/src/graphql_query_builder.rs b/libs/federation_query_planner/src/graphql_query_builder.rs new file mode 100644 index 00000000..086eb5af --- /dev/null +++ b/libs/federation_query_planner/src/graphql_query_builder.rs @@ -0,0 +1,186 @@ +use std::collections::{HashMap, HashSet}; + +use crate::{ + query_planner::{contains_entities_query, EntityQueryNeeds}, + user_query::{FieldNode, OperationType, UserQuery}, +}; + +pub fn generate_entities_query(typename: &str, selection_set: &str) -> String { + assert!( + !typename.is_empty(), + "Typename of the parent field must not be empty when generating an _entity query!" + ); + format!( + "_entities(representations: $representations) {{ ... on {} {{ {} __typename }} }}", + typename, selection_set + ) +} + +pub fn generate_query_for_field( + operation_type: String, + sub_query: String, + // arguments: Vec, + // fragments: &Fragments, +) -> String { + if contains_entities_query(&sub_query) { + // TODO: clean this up + format!( + "{} Entity($representations: [_Any!]!) {{ {} }}", + if operation_type.is_empty() { + "query" + } else { + &operation_type + }, + sub_query + ) + } else { + // let arguments = if !arguments.is_empty() { + // format!("({})", stringify_arguments(&arguments)) + // } else { + // String::new() + // }; + // TODO: add arguments + format!("{}{{ {} }}", operation_type, sub_query) + } +} + +// TODO: VERY EXPENSIVE, NOT GREAT +pub fn batch_subqueries( + input_structures: Vec<(String, String, EntityQueryNeeds)>, +) -> Vec<(String, String, EntityQueryNeeds)> { + let mut batched_subqueries: HashMap, EntityQueryNeeds)> = HashMap::new(); + let mut order: Vec = Vec::new(); + + // Process each structure + for (service, subquery, entity_map) in input_structures { + let parts: Vec<&str> = subquery.split('#').collect(); + + // If the structure matches the entity#fields format, process it + if parts.len() == 2 { + let entity_identifier = parts[0].trim().to_string(); + let fields = parts[1].trim().to_string(); + + let key = format!("{}_{}", service, entity_identifier); + if !batched_subqueries.contains_key(&key) { + order.push(key.clone()); + } + batched_subqueries + .entry(key) + .or_insert_with(|| (HashSet::new(), entity_map)) + .0 + .insert(fields); + } else { + if !batched_subqueries.contains_key(&service) { + order.push(service.clone()); + } + batched_subqueries + .entry(service.clone()) + .or_insert_with(|| (HashSet::new(), entity_map)) + .0 + .insert(subquery); + } + } + + // Convert the HashMap into the desired Vec format based on order + let mut results: Vec<(String, String, EntityQueryNeeds)> = Vec::new(); + for key in order { + let (fields_set, entity_map) = batched_subqueries.remove(&key).unwrap(); + + let service_parts: Vec<&str> = key.split('_').collect(); + let service = service_parts[0]; + let entity = if service_parts.len() > 1 { + service_parts[1] + } else { + "" + }; + + if !entity.is_empty() { + let batched_query = generate_entities_query( + entity, + &fields_set.iter().cloned().collect::>().join(" "), + ); + results.push((service.to_string(), batched_query, entity_map)); + } else { + for field in fields_set { + results.push((service.to_string(), field, entity_map.clone())); + } + } + } + + results +} + +pub fn batch_subqueries_in_user_query(user_query: &mut UserQuery) { + // Recursively process the fields + process_and_batch_subqueries(&mut user_query.fields); +} + +fn process_and_batch_subqueries(fields: &mut [FieldNode]) { + for entity_query in fields.iter_mut() { + if let Some(relevant_sub_queries) = &entity_query.relevant_sub_queries { + let mut batched_subqueries: HashMap> = HashMap::new(); + let mut order: Vec = Vec::new(); + + for (service, subquery) in relevant_sub_queries { + let parts: Vec<&str> = subquery.split('#').collect(); + + // If the structure matches the entity#fields format, process it + if parts.len() == 2 { + let entity_identifier = parts[0].trim().to_string(); + let fields = parts[1].trim().to_string(); + + let key = format!("{}_{}", service, entity_identifier); + if !batched_subqueries.contains_key(&key) { + order.push(key.clone()); + } + batched_subqueries.entry(key).or_default().insert(fields); + } else { + if !batched_subqueries.contains_key(service) { + order.push(service.clone()); + } + batched_subqueries + .entry(service.clone()) + .or_default() + .insert(subquery.to_string()); + } + } + + // Convert the HashMap into the desired Vec format based on order + let mut results: Vec<(String, String)> = Vec::new(); + for key in order.clone() { + let fields_set = batched_subqueries.remove(&key).unwrap(); + + let service_parts: Vec<&str> = key.split('_').collect(); + let service = service_parts[0]; + let entity = if service_parts.len() > 1 { + service_parts[1] + } else { + "" + }; + + if !entity.is_empty() { + let batched_query = generate_entities_query( + entity, + &fields_set.iter().cloned().collect::>().join(" "), + ); + results.push(( + service.to_string(), + generate_query_for_field(OperationType::Query.to_string(), batched_query), + )); + } else { + for field in fields_set { + results.push(( + service.to_string(), + generate_query_for_field(OperationType::Query.to_string(), field), + )); + } + } + } + + entity_query.relevant_sub_queries = Some(results); + + // Recursively process nested fields + process_and_batch_subqueries(&mut entity_query.children); + } + } +} diff --git a/libs/federation_query_planner/src/lib.rs b/libs/federation_query_planner/src/lib.rs new file mode 100644 index 00000000..5bbc6e4f --- /dev/null +++ b/libs/federation_query_planner/src/lib.rs @@ -0,0 +1,330 @@ +use std::ops::Index; + +use anyhow::{Ok, Result}; +use graphql_parser::query::Document; +use serde_json::json; +use supergraph::Supergraph; + +use crate::{ + executor::execute_query_plan, query_planner::plan_for_user_query, user_query::parse_user_query, +}; + +pub mod constants; +pub mod executor; +pub mod graphql_query_builder; +pub mod query_planner; +pub mod supergraph; +pub mod type_merge; +pub mod user_query; + +pub async fn execute_federation( + supergraph: &Supergraph, + parsed_user_query: Document<'static, String>, +) -> Result { + // println!("parsed_user_query: {:#?}", user_query); + let mut user_query = parse_user_query(parsed_user_query)?; + let query_plan = plan_for_user_query(supergraph, &mut user_query)?; + + // println!("query plan: {:#?}", query_plan); + + let response_vec = execute_query_plan(&query_plan, supergraph).await?; + + // println!("response: {:#?}", json!(response_vec).to_string()); + + Ok(json!(response_vec.index(0).index(0).1).to_string()) +} + +#[cfg(test)] +mod tests { + + #[tokio::test] + async fn generates_query_plan() { + use crate::{ + query_planner::plan_for_user_query, supergraph::parse_supergraph, + user_query::parse_user_query, + }; + + let query = r#" + fragment User on User { + id + username + name + } + + fragment Review on Review { + id + body + } + + fragment Product on Product { + inStock + price + shippingEstimate + upc + weight + name + } + + query TestQuery { + users { + ...User + reviews { + ...Review + product { + ...Product + reviews { + ...Review + } + } + } + } + } + "#; + + let supergraph_schema = r#"schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + } + + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + directive @join__unionMember( + graph: join__Graph! + member: String! + ) repeatable on UNION + + directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] + ) repeatable on SCHEMA + + scalar join__FieldSet + + enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:5000/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:5001/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:5002/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:5003/graphql") + } + + scalar link__Import + + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + + type Product + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + } + + type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + user(id: ID!): User @join__field(graph: ACCOUNTS) + users: [User] @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) + } + + type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String + product: Product + author: User @join__field(graph: REVIEWS, provides: "username") + } + + type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + birthday: Int @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + } + "# + .to_string(); + + let _supergraph = parse_supergraph(&supergraph_schema).unwrap(); + let _user_query = parse_user_query(graphql_parser::parse_query(query).unwrap()); + + let supergraph_schema = r#"schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:5000/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:5001/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:5002/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:5003/graphql") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Product + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + user(id: ID!): User @join__field(graph: ACCOUNTS) + users: [User] @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String + product: Product + author: User @join__field(graph: REVIEWS, provides: "username") +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + birthday: Int @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) +} +"# + .to_string(); + + let supergraph = parse_supergraph(&supergraph_schema).unwrap(); + let mut user_query = parse_user_query(graphql_parser::parse_query(query).unwrap()).unwrap(); + + let query_plan = plan_for_user_query(&supergraph, &mut user_query).unwrap(); + + insta::assert_json_snapshot!(query_plan); + } +} diff --git a/libs/federation_query_planner/src/query_planner.rs b/libs/federation_query_planner/src/query_planner.rs new file mode 100644 index 00000000..07d4caee --- /dev/null +++ b/libs/federation_query_planner/src/query_planner.rs @@ -0,0 +1,655 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, vec}; + +use crate::{ + constants::CONDUCTOR_INTERNAL_SERVICE_RESOLVER, + graphql_query_builder::{batch_subqueries, generate_query_for_field}, + supergraph::{GraphQLType, Supergraph}, + user_query::{FieldNode, GraphQLFragment, UserQuery}, +}; + +pub type EntityQueryNeeds = Option; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryStep { + pub service_name: String, + pub query: String, + pub arguments: Option>, + pub entity_query_needs: EntityQueryNeeds, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EntityQuerySearch { + pub __typename: String, + pub fields: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum Parallel { + Sequential(Vec), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct QueryPlan { + pub parallel_steps: Vec, +} + +// fn extract_required_fields_from_requires_string(requires: &str) -> Vec { +// let parsed_query = parse_query::(requires).expect("Failed to parse requires string"); + +// let mut fields = Vec::new(); + +// for definition in parsed_query.definitions { +// match definition { +// Definition::Operation(operation) => match operation { +// OperationDefinition::Query(q) => { +// fields.extend(extract_fields_from_selection(&q.selection_set.items)); +// } +// OperationDefinition::Mutation(m) => { +// fields.extend(extract_fields_from_selection(&m.selection_set.items)); +// } +// OperationDefinition::Subscription(s) => { +// fields.extend(extract_fields_from_selection(&s.selection_set.items)); +// } +// OperationDefinition::SelectionSet(e) => { +// fields.extend(extract_fields_from_selection(&e.items)); +// } +// }, +// _ => {} +// } +// } + +// fields +// } + +// fn extract_fields_from_selection(selection_set: &Vec>) -> Vec { +// let mut fields = Vec::new(); + +// for selection in selection_set { +// if let Selection::Field(Field { name, .. }) = selection { +// fields.push(name.clone()); +// } +// } + +// fields +// } + +fn build_intermediate_structure( + graphql_type: &GraphQLType, + supergraph: &Supergraph, + fields: &mut Vec, + parent_type_name: Option<&str>, + fragments: &HashMap, +) -> Result<()> { + let mut idx = 0; + + while idx < fields.len() { + let field = &mut fields[idx]; + // field.children.sort_by(|a, b| b.owner.cmp(&a.owner)); + + // Handle fragment spreads + if let Some(fragment_name) = field.field.split("...").nth(1) { + let (graphql_type_name, fragment) = fragments + .iter() + .find(|(key, _)| key == &fragment_name) + .unwrap_or_else(|| panic!("{} fragment is not defined in your query!", fragment_name)); + let mut fragment_fields = fragment.fields.clone(); + + let next_gql_type: &GraphQLType = match supergraph.types.get(graphql_type_name) { + Some(t) => t, + None => { + return Err(anyhow!(format!( + "Fragment object type \"{}\" not found in supergraph", + graphql_type_name + ))) + } + }; + + build_intermediate_structure( + next_gql_type, + supergraph, + &mut fragment_fields, + None, + fragments, + )?; + + fields.remove(idx); + fields.splice(idx..idx, fragment_fields.drain(..)); + continue; + } else { + idx += 1; + + if field.field == "__typename" { + // Special handling for __typename field: + // Just continue to the next iteration, + // as __typename is a meta field that doesn't need further resolution. + continue; + } else if field.is_introspection { + // In the case of detecting an introspection query + // Handle introspection queries internally + field.sources = vec![String::from(CONDUCTOR_INTERNAL_SERVICE_RESOLVER)]; + } else { + let gql_field = match graphql_type.fields.get(&field.field) { + Some(f) => f, + None => { + return Err(anyhow!(format!( + "Field \"{}\" is not available on type {}", + field.field, + parent_type_name.unwrap_or("Query") + ))) + } + }; + + let child_type_name = unwrap_graphql_type(&gql_field.field_type); + + field.parent_type_name = parent_type_name.map(String::from); + field.sources = gql_field.sources.clone(); + if graphql_type.owner.is_some() + && field.sources.contains(graphql_type.owner.as_ref().unwrap()) + { + field.owner = graphql_type.owner.clone(); + } + + field.type_name = Some(unwrap_graphql_type(gql_field.field_type.as_str()).to_string()); + field.key_fields = graphql_type.key_fields.clone(); + field.requires = gql_field.requires.clone(); + + if !field.children.is_empty() { + let new_field = FieldNode { + field: String::from("__typename"), + alias: None, + arguments: vec![], + children: vec![], + sources: field.sources.clone(), + type_name: None, + parent_type_name: None, + key_fields: None, + owner: None, + requires: None, + should_be_cleaned: true, // clean it in the response merging phase + relevant_sub_queries: None, + is_introspection: false, + }; + + field.children.push(new_field); + + let next_gql_type: &GraphQLType = match supergraph.types.get(child_type_name) { + Some(t) => t, + None => { + return Err(anyhow!(format!( + "Type \"{}\" not found in supergraph", + child_type_name + ))) + } + }; + + build_intermediate_structure( + next_gql_type, + supergraph, + &mut field.children, + Some(unwrap_graphql_type(gql_field.field_type.as_str())), + fragments, + )?; + } + } + } + + // TODO: add back `requires` fields. + // if let Some(required_fields_string) = &field.requires { + // let required_fields = extract_required_fields_from_requires_string(&format!( + // "{{{}}}", + // required_fields_string + // )); + + // for required_field in required_fields { + // // Check if this required_field is already a child of the parent + // if !field + // .children + // .iter() + // .any(|child| child.field == required_field) + // { + // let supergraph_parent_type = supergraph + // .types + // .get(field.parent_type_name.as_ref().unwrap()); + // let that_required_field = supergraph_parent_type + // .and_then(|gql_type| gql_type.fields.get(&required_field)) + // .expect(&format!( + + // "requires field {} doesn't exist on the parent type {}, your supergraph schema has an error!", + // required_field, parent_type_name.unwrap() + // )); + + // let new_field = FieldNode { + // field: required_field, + // alias: None, + // arguments: vec![], + // children: vec![], + // sources: that_required_field.sources.clone(), + // type_name: Some(that_required_field.field_type.clone()), + // parent_type_name: field.parent_type_name.clone(), + // key_fields: supergraph_parent_type.unwrap().key_fields.clone(), + // owner: supergraph_parent_type.unwrap().owner.clone(), + // requires: that_required_field.requires.clone(), + // should_be_cleaned: true, // clean it in the response merging phase + // }; + // field.children.push(new_field); + // } + // } + // } + } + + Ok(()) +} + +pub fn plan_for_user_query( + supergraph: &Supergraph, + user_query: &mut UserQuery, +) -> Result { + let (_name, query_fields) = supergraph + .types + .iter() + .find(|(name, _t)| name == &"Query") + .expect( + // TODO: should be handled at startup instead + "Query type object is not defined in your supergraph schema!", + ); + + build_intermediate_structure( + query_fields, + supergraph, + &mut user_query.fields, + None, + &user_query.fragments, + )?; + + let mut mappings: Vec<(String, String, EntityQueryNeeds)> = vec![]; + + for field in &mut user_query.fields { + build_fields_mappings_to_subgraphs(field, None, &mut mappings, supergraph); + } + + // TODO: that `.rev()` might be expensive! + let mappings = batch_subqueries(mappings.into_iter().rev().collect()); + + // TODO: uncomment this + // batch_subqueries_in_user_query(user_query); + // fs::write( + // "user-query.json", + // serde_json::to_string(user_query).unwrap(), + // ); + + let steps: Parallel = Parallel::Sequential( + mappings + .into_iter() + .map(|(subgraph, e, entity_query_needs)| QueryStep { + arguments: None, + query: generate_query_for_field(user_query.operation_type.to_string(), e), + service_name: subgraph.clone(), + entity_query_needs, + }) + .collect::>(), + ); + + Ok(QueryPlan { + parallel_steps: vec![steps], + }) +} + +fn build_fields_mappings_to_subgraphs( + field: &mut FieldNode, + parent_source: Option<&str>, + results: &mut Vec<(String, String, EntityQueryNeeds)>, + supergraph: &Supergraph, +) { + resolve_children( + field, + parent_source, + results, + false, + supergraph, + (None, &mut None), + ); +} + +type ParentInfo<'a> = (Option, &'a mut Option>); + +fn resolve_children( + field: &mut FieldNode, + parent_source: Option<&str>, + results: &mut Vec<(String, String, EntityQueryNeeds)>, + nested: bool, + _supergraph: &Supergraph, + (persisted_parent_type_name, shared_parent_type_name_field): ParentInfo, +) -> String { + let current_source = determine_owner(&field.sources, field.owner.as_ref(), parent_source); + + let children_results: Vec<_> = field + .children + .iter_mut() + .filter_map(|e| { + let source = determine_owner(&e.sources, e.owner.as_ref(), parent_source); + + let res = resolve_children( + e, + Some(¤t_source), + results, + source == current_source, + _supergraph, + if persisted_parent_type_name == field.parent_type_name { + ( + persisted_parent_type_name.clone(), + shared_parent_type_name_field, + ) + } else { + ( + field.parent_type_name.clone(), + &mut field.relevant_sub_queries, + ) + }, + ); + + if source != current_source || res.is_empty() { + None + } else { + Some(res) + } + }) + .collect(); + + // Return an empty string if the field has no valid children and is not itself a source + // if children_results.is_empty() && !field.sources.contains(¤t_source) { + // return String::with_capacity(0); + // } + + let res = if children_results.is_empty() { + field.field.to_string() + } else { + format!("{} {{ {} }}", field.field, children_results.join(" ")) + }; + + if !nested && !res.is_empty() { + let current_source_str = current_source.to_string(); + + let (result, entity_key_map) = if field.key_fields.is_some() + // don't do an entity query on a root Query resolvable field + && field.parent_type_name.is_some() + { + // If no children, populate the current field + if !field.children.is_empty() { + field.relevant_sub_queries.get_or_insert(vec![]).push(( + current_source_str.clone(), + format!("{}#{}", field.parent_type_name.as_ref().unwrap(), &res), + )); + } else { + shared_parent_type_name_field.get_or_insert(vec![]).push(( + current_source_str.clone(), + format!("{}#{}", field.parent_type_name.as_ref().unwrap(), &res), + )); + } + ( + format!("{}#{}", field.parent_type_name.as_ref().unwrap(), &res), + Some(EntityQuerySearch { + __typename: field.parent_type_name.as_ref().unwrap().clone(), + fields: vec![field.key_fields.as_ref().unwrap().clone()], + }), + ) + } else { + field.relevant_sub_queries = Some(vec![(current_source_str.clone(), res.clone())]); + (res.clone(), None) + }; + + // if let Some(graphql_type) = supergraph.types.get(field.type_name.as_ref().unwrap()) { + // ensure_key_fields_included_for_type( + // graphql_type, + // &mut results.get_mut(¤t_source).unwrap(), + // ); + // } + + results.push((current_source_str, result, entity_key_map)); + } + res +} + +fn determine_owner( + field_sources: &[String], + owner: Option<&String>, + parent_source: Option<&str>, +) -> String { + // 1. Check if there's only one join, if yes, just return it + if field_sources.len() == 1 { + return field_sources.first().unwrap().clone(); + } + + // 2. Check if it has an owner defined + if let Some(owner_str) = owner { + return owner_str.to_string(); + } + + // 3. Check if the parent source is present in field sources and return it + if let Some(p) = parent_source { + let parent_soruce_str = p.to_string(); + if field_sources.contains(&parent_soruce_str) { + return parent_soruce_str; + } + } + + // 4. If no match for parent source, return the first one as default + field_sources.first().cloned().expect("No sources found") +} + +pub fn contains_entities_query(field_strings: &str) -> bool { + field_strings.contains("_entities(representations: $representations)") +} + +pub fn get_type_info_of_field<'a>( + field_name: &'a str, + supergraph: &'a Supergraph, +) -> (Option<&'a GraphQLType>, Option<&'a str>) { + for (type_name, type_def) in &supergraph.types { + if let Some(field_def) = type_def.fields.get(field_name) { + return ( + supergraph + .types + .get(unwrap_graphql_type(&field_def.field_type)), + Some(type_name), + ); + } + } + (None, None) +} + +// fn build_queries_services_map<'a>( +// field: &FieldNode, +// fragments: &Fragments, +// selection_set: &mut FieldSelectionSet, +// ) { +// let (field_type, field_type_name) = get_type_info_of_field(&field.field, &field.supergraph); +// let field_type = match field_type { +// Some(ft) => ft, +// None => return, +// }; + +// // Use the provided parent_type_name or the one from get_type_info_of_field +// let field_type_name = field.parent_type_name.or(field_type_name); + +// for subfield in &field.children { +// let is_fragment = subfield.field.starts_with("..."); + +// if is_fragment { +// let fragment_name = subfield +// .field +// .split("...") +// .nth(1) +// .expect("Incorrect fragment usage!"); +// let fragment_fields = fragments.get(fragment_name).expect(&format!( +// "The used \"{}\" Fragment is not defined!", +// &fragment_name +// )); + +// for frag_field in &fragment_fields.fields { +// let fragment_query = process_field(frag_field, &field.supergraph, fragments); + +// if let Some(field_def) = field_type.fields.get(&frag_field.field) { +// selection_set.add_field(&field_def.source, fragment_query); +// } +// } +// } else if let Some(field_def) = field_type.fields.get(&subfield.field) { +// let subfield_selection = if subfield.children.is_empty() { +// subfield.field.clone() +// } else { +// build_queries_services_map(subfield, fragments, selection_set); +// format!( +// "{} {{ {} }}", +// subfield.field, +// selection_set +// .get_fields_for_service(&field_def.source) +// .unwrap_or(&vec![]) +// .join(" ") +// ) +// }; + +// // Get the actual type name of the field. +// let actual_typename = +// get_type_name_of_field(subfield.field.to_string(), None, &field.supergraph) +// .unwrap_or_default(); + +// let entity_typename = +// get_type_name_of_field(field.field.to_string(), None, &field.supergraph) +// .unwrap_or_default() +// .to_string(); + +// let key_fields_option = field.supergraph.types.get(&actual_typename); + +// if let Some(type_info) = field +// .supergraph +// .types +// .get(&unwrap_graphql_type(&field_def.field_type)) +// { +// if !type_info.key_fields.is_empty() { +// // Generate entities query using the entity_typename +// let new_query = generate_entities_query(entity_typename, subfield_selection); +// selection_set.add_field(&field_def.source, new_query); +// continue; +// } +// } + +// selection_set.add_field(&field_def.source, subfield_selection); + +// // Ensure that key fields are included in the selections if not already present +// if let Some(graphql_type) = +// get_type_of_field(field.field.to_string(), None, &field.supergraph) +// { +// ensure_key_fields_included_for_type( +// graphql_type, +// selection_set +// .fields +// .entry(field_def.source.clone()) +// .or_insert_with(Vec::new), +// ); +// } + +// // Add __typename to the selection set for the type +// if let Some(field_selections) = selection_set.get_fields_for_service(&field_def.source) +// { +// if !field_selections.contains(&"__typename".to_string()) { +// selection_set.add_field(&field_def.source, "__typename".to_string()); +// } +// } +// } +// } +// } + +// fn process_field<'a>(subfield: &FieldNode, supergraph: &Supergraph) -> String { +// if subfield.children.is_empty() { +// return subfield.field.clone(); +// } + +// let nested_fields = subfield +// .children +// .iter() +// .map(|child| process_field(child, supergraph)) +// .collect::>() +// .join(" "); + +// format!("{} {{ {} }}", subfield.field, nested_fields) +// } + +// fn ensure_key_fields_included_for_type<'a>( +// graphql_type: &GraphQLType, +// current_selections: &mut String, +// ) { +// // Skip if it's an entities query +// if contains_entities_query(¤t_selections) { +// return; +// } + +// // Create a new vector to hold selections in the correct order +// let mut new_selections = Vec::new(); + +// // First, add key fields (if they aren't already in the current selections) +// for key_field in &graphql_type.key_fields { +// if !current_selections.contains(key_field) { +// new_selections.push(key_field.clone()); +// } +// } + +// // Then, add other fields from current_selections +// new_selections.extend(current_selections.iter().cloned()); + +// // Replace current_selections with the new vector +// *current_selections = new_selections; +// } + +pub fn get_type_of_field( + field_name: String, + parent_type: Option, + supergraph: &Supergraph, +) -> Option<&GraphQLType> { + for (type_name, type_def) in &supergraph.types { + // Check if we should restrict by parent type + if let Some(parent) = &parent_type { + if parent != type_name { + continue; + } + } + + if let Some(field_def) = type_def.fields.get(&field_name) { + return supergraph + .types + .get(unwrap_graphql_type(&field_def.field_type)); + } + } + + None +} + +pub fn get_type_name_of_field( + field_name: String, + parent_type: Option, + supergraph: &Supergraph, +) -> Option<&str> { + for (type_name, type_def) in &supergraph.types { + // Check if we should restrict by parent type + if let Some(parent) = &parent_type { + if parent != type_name { + continue; + } + } + + if let Some(field_def) = type_def.fields.get(&field_name) { + return Some(unwrap_graphql_type(&field_def.field_type)); + } + } + + None +} + +fn unwrap_graphql_type(typename: &str) -> &str { + let mut unwrapped = typename; + while unwrapped.ends_with('!') || unwrapped.starts_with('[') || unwrapped.ends_with(']') { + unwrapped = unwrapped.trim_end_matches('!'); + unwrapped = unwrapped.trim_start_matches('['); + unwrapped = unwrapped.trim_end_matches(']'); + } + unwrapped +} diff --git a/libs/federation_query_planner/src/snapshots/federation_query_planner__generates_query_plan.snap b/libs/federation_query_planner/src/snapshots/federation_query_planner__generates_query_plan.snap new file mode 100644 index 00000000..e49fc9ed --- /dev/null +++ b/libs/federation_query_planner/src/snapshots/federation_query_planner__generates_query_plan.snap @@ -0,0 +1,51 @@ +--- +source: libs/federation_query_planner/src/lib.rs +expression: query_plan +--- +{ + "parallel_steps": [ + { + "Sequential": [ + { + "service_name": "ACCOUNTS", + "query": "{ users { id username name __typename } }", + "arguments": null, + "entity_query_needs": null + }, + { + "service_name": "REVIEWS", + "query": "query Entity($representations: [_Any!]!) { _entities(representations: $representations) { ... on User { reviews { id body product { upc reviews { id body __typename } __typename } __typename } __typename } } }", + "arguments": null, + "entity_query_needs": { + "__typename": "User", + "fields": [ + "id" + ] + } + }, + { + "service_name": "PRODUCTS", + "query": "query Entity($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { price name weight __typename } } }", + "arguments": null, + "entity_query_needs": { + "__typename": "Product", + "fields": [ + "upc" + ] + } + }, + { + "service_name": "INVENTORY", + "query": "query Entity($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { shippingEstimate inStock __typename } } }", + "arguments": null, + "entity_query_needs": { + "__typename": "Product", + "fields": [ + "upc" + ] + } + } + ] + } + ] +} diff --git a/libs/federation_query_planner/src/snapshots/federation_query_planner__generates_query_plan.snap.new b/libs/federation_query_planner/src/snapshots/federation_query_planner__generates_query_plan.snap.new new file mode 100644 index 00000000..9fb0bc3b --- /dev/null +++ b/libs/federation_query_planner/src/snapshots/federation_query_planner__generates_query_plan.snap.new @@ -0,0 +1,52 @@ +--- +source: libs/federation_query_planner/src/lib.rs +assertion_line: 198 +expression: query_plan +--- +{ + "parallel_steps": [ + { + "Sequential": [ + { + "service_name": "ACCOUNTS", + "query": "{ users { id username name __typename } }", + "arguments": null, + "entity_query_needs": null + }, + { + "service_name": "REVIEWS", + "query": "query Entity($representations: [_Any!]!) { _entities(representations: $representations) { ... on User { reviews { id body product { upc reviews { id body __typename } __typename } __typename } __typename } } }", + "arguments": null, + "entity_query_needs": { + "__typename": "User", + "fields": [ + "id" + ] + } + }, + { + "service_name": "PRODUCTS", + "query": "query Entity($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { name price weight __typename } } }", + "arguments": null, + "entity_query_needs": { + "__typename": "Product", + "fields": [ + "upc" + ] + } + }, + { + "service_name": "INVENTORY", + "query": "query Entity($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { inStock shippingEstimate __typename } } }", + "arguments": null, + "entity_query_needs": { + "__typename": "Product", + "fields": [ + "upc" + ] + } + } + ] + } + ] +} diff --git a/libs/federation_query_planner/src/supergraph.rs b/libs/federation_query_planner/src/supergraph.rs new file mode 100644 index 00000000..2f6d8553 --- /dev/null +++ b/libs/federation_query_planner/src/supergraph.rs @@ -0,0 +1,167 @@ +use std::{collections::HashMap, error::Error}; + +use graphql_parser::{ + parse_schema, + schema::{Definition as SchemaDefinition, TypeDefinition, Value}, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] +pub struct GraphQLField { + pub field_type: String, + pub sources: Vec, + pub requires: Option, + pub provides: Option, + pub external: bool, +} + +#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] +pub struct GraphQLType { + pub key_fields: Option, + pub fields: HashMap, + pub owner: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Supergraph { + pub types: HashMap, + pub subgraphs: HashMap, +} + +fn get_argument_value(args: &[(String, Value<'_, String>)], key: &str) -> Option { + args + .iter() + .find(|(k, _)| k == key) + .map(|(_, v)| v.to_string().trim().to_string()) +} + +pub fn parse_supergraph(supergraph_schema: &str) -> Result> { + let result = parse_schema::(supergraph_schema)?; + + let mut parsed_supergraph = Supergraph::default(); + + for e in result.definitions { + if let SchemaDefinition::TypeDefinition(t) = e { + match t { + // 1. Get Subgraphs name and their corresponding URLs + TypeDefinition::Enum(a) => { + for mut value in a.values { + // we aren't at the correct subgraphs enum definition if it is empty + if value.directives.is_empty() { + continue; + } + + // Select the first one, because in any supergraph, there will always be just one defining the subgraphs + // We're using `.remove(0)` here to get ownership over the first item, to avoid references + clones + let directive = value.directives.remove(0); + let arguments = directive.arguments; + + // `join__graph` enum contains a map of the subgraphs + if directive.name == "join__graph" { + let name = get_argument_value(&arguments, "name") + .unwrap() + .trim_matches('"') + .to_uppercase(); + let url = get_argument_value(&arguments, "url") + .unwrap() + .trim_matches('"') + .to_string(); + + parsed_supergraph.subgraphs.insert(name, url); + } + } + } + TypeDefinition::Object(obj) => { + // 2. Get each graphql type + let mut graphql_type = GraphQLType::default(); + + // 3. Get the subgraph, the type belongs to, this is useful in cases where the individual fields are not + // annotated with a `@join__field(graph: $SUBGRAPH)`, and all the type's fields belong to the type's subgraph origin + let mut graphql_type_subgraphs = Vec::new(); + + for directive in obj.directives { + match directive.name.as_str() { + "join__type" => { + if let Some(graph) = get_argument_value(&directive.arguments, "graph") { + graphql_type_subgraphs.push(graph); + + // 4. Get entity's keys + if let Some(key) = get_argument_value(&directive.arguments, "key") { + let key = key.to_string().trim_matches('"').to_string(); + graphql_type.key_fields = Some(key); + } + } + } + "join__owner" => { + if let Some(graph) = get_argument_value(&directive.arguments, "graph") { + graphql_type.owner = Some(graph.trim_matches('"').to_string()); + } + } + _ => {} + } + } + + for field in obj.fields { + let mut graphql_type_field = GraphQLField { + sources: graphql_type_subgraphs.clone(), + field_type: field.field_type.to_string(), + requires: None, + provides: None, + external: false, + }; + + for field_directive in field.directives { + if field_directive.name == "join__field" { + for (k, v) in &field_directive.arguments { + match k.as_str() { + // 5. Get the field's subgraph owner + "graph" => { + // if field_directive + // .arguments + // .iter() + // // We're excluding `@join__field(external: true)` because we want the owning subgraph not the one referencing it + // .any(|(key, val)| { + // key == "external" && val.to_string() == "true" + // }) + // { + graphql_type_field.sources = vec![v.to_string()]; + // } + } + // 6. Get other useful directives + "requires" => { + graphql_type_field.requires = + Some(v.to_string().trim_matches('\"').to_string()); + } + "provides" => { + graphql_type_field.provides = Some(v.to_string()); + } + "external" => { + graphql_type_field.external = v.to_string() == "true"; + } + _ => {} + } + } + } + } + + graphql_type + .fields + .insert(field.name.clone(), graphql_type_field); + } + + parsed_supergraph + .types + .insert(obj.name.clone(), graphql_type); + } + _ => {} + } + } + } + + if parsed_supergraph.subgraphs.is_empty() || parsed_supergraph.types.is_empty() { + return Err("Your Supergraph Schema doesn't seem to be correct! The Parser has resulted in 0 types, and 0 subgraphs.".into()); + } + + Ok(parsed_supergraph) +} diff --git a/libs/federation_query_planner/src/type_merge.rs b/libs/federation_query_planner/src/type_merge.rs new file mode 100644 index 00000000..f3bc6085 --- /dev/null +++ b/libs/federation_query_planner/src/type_merge.rs @@ -0,0 +1,94 @@ +use serde_json::{json, Map, Value}; + +use crate::{ + executor::QueryResponse, + user_query::{FieldNode, UserQuery}, +}; + +// Helper function to correctly nest the child value into the result. +fn nest_value(result: &mut Value, path: Vec<&str>, child_value: Value) { + // Start from the root of the result which is a Value + let mut current = result; + + // Iterate through the path except for the last element + for key in path.iter().take(path.len() - 1) { + let key_str = (*key).to_string(); // Convert &str to String + + // Navigate through or create the nested objects as needed + let entry = current + .as_object_mut() + .expect("Should be an object") + .entry(key_str) + .or_insert_with(|| Value::Object(Map::new())); + + current = entry; // Move our reference down to this level + } + + // Insert the final value at the end of the path + if let Some(last_key) = path.last() { + if let Some(obj) = current.as_object_mut() { + obj.insert(last_key.to_string(), child_value); // Use the last element from the path as the key + } + } +} + +pub fn construct_user_response( + user_query: UserQuery, + responses: Vec>, +) -> String { + let mut response_data = Value::Object(Map::new()); // Start with a Value::Object instead of a raw Map + + for field in &user_query.fields { + // This needs to recursively construct the response with nesting + construct_field_response(field, &responses, &mut response_data, Vec::new()); + } + + json!({ "data": response_data }).to_string() +} + +fn construct_field_response( + field: &FieldNode, + responses: &Vec>, + result: &mut Value, // Change type to &mut Value + path: Vec<&str>, +) { + if field.should_be_cleaned { + return; + } + + let mut current_path = path.clone(); + let field_name = field.alias.as_ref().unwrap_or(&field.field); + current_path.push(field_name); + + if let Some(relevant_queries) = &field.relevant_sub_queries { + for (source, sub_query) in relevant_queries { + if let Some(sub_response) = find_response(responses, source, sub_query) { + if let Some(sub_response_data) = &sub_response.data { + // Now, instead of inserting directly, we need to nest the value + nest_value(result, current_path.clone(), sub_response_data.clone()); + } + } + } + } + + // Recursively construct responses for nested fields + for child_field in &field.children { + construct_field_response(child_field, responses, result, current_path.clone()); + } +} + +fn find_response<'a>( + responses: &'a Vec>, + source: &'a str, + sub_query: &'a str, +) -> Option<&'a QueryResponse> { + for response_group in responses { + for ((response_source, response_query), response) in response_group { + if response_source == source && response_query.ends_with(sub_query) { + // The endswith check is a simplification. In practice, you might need a more robust comparison + return Some(response); + } + } + } + None +} diff --git a/libs/federation_query_planner/src/user_query.rs b/libs/federation_query_planner/src/user_query.rs new file mode 100644 index 00000000..fd50eca9 --- /dev/null +++ b/libs/federation_query_planner/src/user_query.rs @@ -0,0 +1,256 @@ +use anyhow::{Ok, Result}; +use graphql_parser::query::{Definition, Document, Field, OperationDefinition, Selection}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fmt::{Display, Formatter}, +}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FieldNode { + pub field: String, + pub alias: Option, + pub arguments: Vec, + pub children: Vec, + pub sources: Vec, + pub type_name: Option, + pub parent_type_name: Option, + pub key_fields: Option, + pub owner: Option, + pub requires: Option, + pub should_be_cleaned: bool, + pub relevant_sub_queries: Option>, + pub is_introspection: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum OperationType { + Query, + Mutation, + Subscription, +} +impl Display for OperationType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + // can be `query`, but why not save some space + OperationType::Query => write!(f, ""), + OperationType::Mutation => write!(f, "mutation"), + OperationType::Subscription => write!(f, "subscription"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct QueryArgument { + pub name: String, + pub value: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct QueryDefinedArgument { + pub name: String, + pub default_value: Option, +} + +type QueryDefinedArguments = Vec; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GraphQLFragment { + pub str_definition: String, + pub fields: Vec, +} + +pub type Fragments = HashMap; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserQuery { + // Note: We don't currently need to track the operation name, it's useless for query planning, can be useful for analytics later on... + pub operation_type: OperationType, + pub arguments: Vec, + pub fields: Vec, + pub fragments: Fragments, +} + +fn seek_root_fields_capacity(parsed_query: &Document<'_, String>) -> usize { + parsed_query + .definitions + .iter() + .find_map(|e| match e { + Definition::Operation(val) => match val { + OperationDefinition::Query(e) => Some(e.selection_set.items.len()), + OperationDefinition::Mutation(e) => Some(e.selection_set.items.len()), + OperationDefinition::Subscription(e) => Some(e.selection_set.items.len()), + OperationDefinition::SelectionSet(e) => Some(e.items.len()), + }, + _ => None, + }) + .unwrap_or(0) +} + +pub fn parse_user_query(parsed_query: Document<'static, String>) -> Result { + let mut user_query = UserQuery { + operation_type: OperationType::Query, + arguments: vec![], + fields: Vec::with_capacity(seek_root_fields_capacity(&parsed_query)), + fragments: HashMap::new(), + }; + + for definition in parsed_query.definitions { + match definition { + Definition::Operation(OperationDefinition::Query(q)) => { + user_query.operation_type = OperationType::Query; + + user_query.arguments = q + .variable_definitions + .into_iter() + .map(|e| QueryDefinedArgument { + name: e.name, + default_value: e.default_value.map(|e| e.to_string()), + }) + .collect::>(); + + user_query.fields.extend(handle_selection_set( + &user_query.arguments, + q.selection_set, + )?); + } + Definition::Operation(OperationDefinition::Mutation(m)) => { + user_query.operation_type = OperationType::Mutation; + + user_query.arguments = m + .variable_definitions + .into_iter() + .map(|e| QueryDefinedArgument { + name: e.name, + default_value: e.default_value.map(|e| e.to_string()), + }) + .collect::>(); + + user_query.fields.extend(handle_selection_set( + &user_query.arguments, + m.selection_set, + )?); + } + Definition::Operation(OperationDefinition::Subscription(s)) => { + user_query.operation_type = OperationType::Subscription; + + user_query.arguments = s + .variable_definitions + .into_iter() + .map(|e| QueryDefinedArgument { + name: e.name, + default_value: e.default_value.map(|e| e.to_string()), + }) + .collect::>(); + + user_query.fields.extend(handle_selection_set( + &user_query.arguments, + s.selection_set, + )?); + } + Definition::Operation(OperationDefinition::SelectionSet(e)) => { + user_query.fields = handle_selection_set(&user_query.arguments, e)?; + } + Definition::Fragment(e) => { + user_query.fragments.insert( + e.name.to_string(), + GraphQLFragment { + str_definition: format!("{}", e), + fields: handle_selection_set(&user_query.arguments, e.selection_set)?, + }, + ); + } + } + } + + Ok(user_query) +} + +fn handle_selection_set( + defined_arguments: &QueryDefinedArguments, + selection_set: graphql_parser::query::SelectionSet<'_, String>, +) -> Result> { + let mut fields = Vec::with_capacity(selection_set.items.len()); + + for selection in selection_set.items { + match selection { + Selection::Field(Field { + name, + selection_set: field_selection_set, + arguments, + alias, + .. + }) => { + let is_introspection = name.starts_with("__"); + let (name, children) = if is_introspection { + (format!("{name}{}", field_selection_set), vec![]) + } else { + ( + name, + handle_selection_set(defined_arguments, field_selection_set)?, + ) + }; + + let arguments = arguments + .into_iter() + .map(|(arg_name, value)| { + let value = value.to_string(); + let value = if value.starts_with('$') { + defined_arguments + .iter() + .find(|e| e.name == value[1..]) + .unwrap_or_else(|| panic!("Argument {} is used but was never defined!", value)) + .default_value + .as_ref() + .unwrap_or_else(|| panic!("No default value for {}!", value)) + .to_string() + } else { + value + }; + + QueryArgument { + name: arg_name, + value, + } + }) + .collect(); + + fields.push(FieldNode { + field: name, + children, + alias, + arguments, + parent_type_name: None, + sources: vec![], + type_name: None, + key_fields: None, + owner: None, + requires: None, + should_be_cleaned: false, + relevant_sub_queries: None, + is_introspection, + }); + } + Selection::FragmentSpread(e) => { + fields.push(FieldNode { + field: format!("...{}", e.fragment_name), + children: vec![], + alias: None, + arguments: vec![], + parent_type_name: None, + sources: vec![], + type_name: None, + key_fields: None, + owner: None, + requires: None, + should_be_cleaned: false, + relevant_sub_queries: None, + is_introspection: false, + }); + } + _ => {} + } + } + + Ok(fields) +} diff --git a/test_config/config.yaml b/test_config/config.yaml index 558eb331..043f0dac 100644 --- a/test_config/config.yaml +++ b/test_config/config.yaml @@ -10,6 +10,12 @@ sources: config: endpoint: ${COUNTRIES_ENDPOINT:https://countries.trevorblades.com/} + - id: fed + type: federation + config: + supergraph: + file: ./supergraph.graphql + endpoints: - path: /graphql from: countries @@ -64,3 +70,8 @@ endpoints: - type: http_get config: mutations: false + + - path: /federation + from: fed + plugins: + - type: graphiql diff --git a/test_config/supergraph.graphql b/test_config/supergraph.graphql new file mode 100644 index 00000000..550eecad --- /dev/null +++ b/test_config/supergraph.graphql @@ -0,0 +1,119 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Product + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String + author: User @join__field(graph: REVIEWS, provides: "username") + product: Product +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: [Review] @join__field(graph: REVIEWS) +} diff --git a/test_config/worker.yaml b/test_config/worker.yaml index 8f3dd9fc..3752447a 100644 --- a/test_config/worker.yaml +++ b/test_config/worker.yaml @@ -7,6 +7,12 @@ sources: config: endpoint: https://countries.trevorblades.com/ + - id: gateways-benchmark + type: federation + config: + supergraph: + file: ./temp/supergraph.graphql + endpoints: - path: /graphql from: countries