From 3c99947510df901e7bf07ec6256bfc3aaee8b575 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:47:20 +0000 Subject: [PATCH 01/16] fix(deps): update tokio-prost monorepo to 0.13.0 (#2854) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Tushar Mathur --- Cargo.lock | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 530a3206b2..766b036ff0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2769,15 +2769,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.14" @@ -4111,12 +4102,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "prost-derive 0.13.3", + "prost-derive 0.13.4", ] [[package]] @@ -4155,12 +4146,12 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.90", @@ -4189,7 +4180,7 @@ dependencies = [ "logos", "miette 7.4.0", "once_cell", - "prost 0.13.3", + "prost 0.13.4", "prost-types 0.13.3", "serde", "serde-value", @@ -4210,7 +4201,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" dependencies = [ - "prost 0.13.3", + "prost 0.13.4", ] [[package]] @@ -4350,7 +4341,7 @@ checksum = "873f359bdecdfe6e353752f97cb9ee69368df55b16363ed2216da85e03232a58" dependencies = [ "bytes", "miette 7.4.0", - "prost 0.13.3", + "prost 0.13.4", "prost-reflect 0.14.2", "prost-types 0.13.3", "protox-parse 0.7.0", @@ -5668,7 +5659,7 @@ dependencies = [ "pluralizer", "pretty_assertions", "prometheus", - "prost 0.13.3", + "prost 0.13.4", "prost-reflect 0.14.2", "protox 0.7.1", "protox-parse 0.7.0", @@ -6393,7 +6384,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0081d8ee0847d01271392a5aebe960a4600f5d4da6c67648a6382a0940f8b367" dependencies = [ - "prost 0.13.3", + "prost 0.13.4", "prost-types 0.13.3", "tonic 0.12.3", ] From b16cd3d72ecfe792b61fc320999d8b771d004607 Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:32:29 +0100 Subject: [PATCH 02/16] fix: tailcall generation schema for non required fields (#3202) --- generated/.tailcallrc.graphql | 16 +++---- generated/.tailcallrc.schema.json | 5 +- src/core/blueprint/operators/graphql.rs | 2 +- src/core/config/directives/graphql.rs | 2 +- .../src/directive_definition.rs | 2 +- .../src/input_definition.rs | 47 ++++++++++--------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index 46e48378af..9f6f170ea6 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -71,7 +71,7 @@ directive @graphQL( argument to batch several requests into a single batch request.Make sure you have also specified batch settings to the `@upstream` and to the `@graphQL` operator. """ - batch: Boolean! + batch: Boolean """ Enables deduplication of IO operations to enhance performance.This flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: @@ -269,7 +269,7 @@ directive @link( The source of the link. It can be a URL or a path to a file. If a path is provided, it is relative to the file that imports the link. """ - src: String! + src: String """ The type of the link. It can be `Config`, or `Protobuf`. """ @@ -728,8 +728,8 @@ input Headers { } input Routes { - graphQL: String! - status: String! + graphQL: String + status: String } input ScriptOptions { @@ -772,7 +772,7 @@ Output the telemetry metrics data to prometheus server """ input PrometheusExporter { format: PrometheusFormat - path: String! + path: String } """ @@ -782,7 +782,7 @@ input StdoutExporter { """ Output to stdout in pretty human-readable format """ - pretty: Boolean! + pretty: Boolean } input TelemetryExporter { @@ -793,7 +793,7 @@ input TelemetryExporter { } input Batch { - delay: Int! + delay: Int headers: [String!] maxSize: Int } @@ -816,7 +816,7 @@ input GraphQL { argument to batch several requests into a single batch request.Make sure you have also specified batch settings to the `@upstream` and to the `@graphQL` operator. """ - batch: Boolean! + batch: Boolean """ Enables deduplication of IO operations to enhance performance.This flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index 78840b9bea..ffb4d75c7e 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -522,10 +522,7 @@ }, "dedupe": { "description": "Enables deduplication of IO operations to enhance performance.\n\nThis flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: May lead to issues with APIs that expect unique results for identical inputs, such as nonce-based APIs.", - "type": [ - "boolean", - "null" - ] + "type": "boolean" }, "headers": { "description": "The headers parameter allows you to customize the headers of the GraphQL request made by the `@graphQL` operator. It is used by specifying a key-value map of header names and their values.", diff --git a/src/core/blueprint/operators/graphql.rs b/src/core/blueprint/operators/graphql.rs index 4fe3189f2b..fa678fb37b 100644 --- a/src/core/blueprint/operators/graphql.rs +++ b/src/core/blueprint/operators/graphql.rs @@ -84,7 +84,7 @@ pub fn compile_graphql( .map(|req_template| { let field_name = graphql.name.clone(); let batch = graphql.batch; - let dedupe = graphql.dedupe.unwrap_or_default(); + let dedupe = graphql.dedupe; IR::IO(IO::GraphQL { req_template, field_name, batch, dl_id: None, dedupe }) }) } diff --git a/src/core/config/directives/graphql.rs b/src/core/config/directives/graphql.rs index 509c2a0646..f9b74bc07e 100644 --- a/src/core/config/directives/graphql.rs +++ b/src/core/config/directives/graphql.rs @@ -55,5 +55,5 @@ pub struct GraphQL { /// concurrently, reducing resource load. Caution: May lead to issues /// with APIs that expect unique results for identical inputs, such as /// nonce-based APIs. - pub dedupe: Option, + pub dedupe: bool, } diff --git a/tailcall-typedefs-common/src/directive_definition.rs b/tailcall-typedefs-common/src/directive_definition.rs index 908e9087c7..c662130e49 100644 --- a/tailcall-typedefs-common/src/directive_definition.rs +++ b/tailcall-typedefs-common/src/directive_definition.rs @@ -80,7 +80,7 @@ pub fn into_directive_definition( TypeSystemDefinition::Directive(pos(async_graphql::parser::types::DirectiveDefinition { description: description.map(|inner| pos(inner.clone())), name: pos(Name::new(name)), - arguments: into_input_value_definition(&schema), + arguments: into_input_value_definition(schema), is_repeatable: attrs.repeatable, locations: attrs .locations diff --git a/tailcall-typedefs-common/src/input_definition.rs b/tailcall-typedefs-common/src/input_definition.rs index 0d8819c8ef..086722e2d2 100644 --- a/tailcall-typedefs-common/src/input_definition.rs +++ b/tailcall-typedefs-common/src/input_definition.rs @@ -14,27 +14,27 @@ pub trait InputDefinition { } pub fn into_input_definition(schema: SchemaObject, name: &str) -> TypeSystemDefinition { - let description = get_description(&schema); + let description = get_description(&schema).cloned(); TypeSystemDefinition::Type(pos(TypeDefinition { name: pos(Name::new(name)), kind: TypeKind::InputObject(InputObjectType { - fields: into_input_value_definition(&schema), + fields: into_input_value_definition(schema), }), - description: description.map(|inner| pos(inner.clone())), + description: description.map(pos), directives: vec![], extend: false, })) } -pub fn into_input_value_definition(schema: &SchemaObject) -> Vec> { +pub fn into_input_value_definition(schema: SchemaObject) -> Vec> { let mut arguments_type = vec![]; if let Some(subschema) = schema.subschemas.clone() { let list = subschema.any_of.or(subschema.all_of).or(subschema.one_of); if let Some(list) = list { for schema in list { let schema_object = schema.into_object(); - arguments_type.extend(build_arguments_type(&schema_object)); + arguments_type.extend(build_arguments_type(schema_object)); } return arguments_type; @@ -44,22 +44,19 @@ pub fn into_input_value_definition(schema: &SchemaObject) -> Vec Vec> { +fn build_arguments_type(schema: SchemaObject) -> Vec> { let mut arguments = vec![]; - if let Some(properties) = schema - .object - .as_ref() - .map(|object| object.properties.clone()) - { - for (name, property) in properties.into_iter() { + if let Some(obj) = schema.object { + let required = obj.required; + for (name, property) in obj.properties.into_iter() { let property = property.into_object(); let description = get_description(&property); + let nullable = !required.contains(&name); let definition = pos(InputValueDefinition { description: description.map(|inner| pos(inner.to_owned())), name: pos(Name::new(&name)), ty: pos(determine_input_value_type_from_schema( - name, - property.clone(), + name, &property, nullable, )), default_value: None, directives: Vec::new(), @@ -72,7 +69,11 @@ fn build_arguments_type(schema: &SchemaObject) -> Vec Type { +fn determine_input_value_type_from_schema( + mut name: String, + schema: &SchemaObject, + nullable: bool, +) -> Type { first_char_to_upper(&mut name); if let Some(instance_type) = &schema.instance_type { match instance_type { @@ -82,10 +83,10 @@ fn determine_input_value_type_from_schema(mut name: String, schema: SchemaObject | InstanceType::Number | InstanceType::String | InstanceType::Integer => Type { - nullable: false, + nullable, base: BaseType::Named(Name::new(get_instance_type_name(typ))), }, - _ => determine_type_from_schema(name, &schema), + _ => determine_type_from_schema(name, schema), }, SingleOrVec::Vec(typ) => match typ.first().unwrap() { InstanceType::Null @@ -93,14 +94,14 @@ fn determine_input_value_type_from_schema(mut name: String, schema: SchemaObject | InstanceType::Number | InstanceType::String | InstanceType::Integer => Type { - nullable: true, + nullable, base: BaseType::Named(Name::new(get_instance_type_name(typ.first().unwrap()))), }, - _ => determine_type_from_schema(name, &schema), + _ => determine_type_from_schema(name, schema), }, } } else { - determine_type_from_schema(name, &schema) + determine_type_from_schema(name, schema) } } @@ -145,14 +146,16 @@ fn determine_type_from_arr_valid(name: String, array_valid: &ArrayValidation) -> nullable: true, base: BaseType::List(Box::new(determine_input_value_type_from_schema( name, - schema.clone().into_object(), + &schema.clone().into_object(), + false, ))), }, SingleOrVec::Vec(schemas) => Type { nullable: true, base: BaseType::List(Box::new(determine_input_value_type_from_schema( name, - schemas[0].clone().into_object(), + &schemas[0].clone().into_object(), + false, ))), }, } From e71dc944de8dc40b675c4fb620f146bfc66bea2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:07:55 +0000 Subject: [PATCH 03/16] fix(deps): update rust crate anyhow to v1.0.94 (#3203) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 766b036ff0..8b8f777c31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "anymap2" From 1e227dfbb4c982fd8c35b0d3755f871b44159fae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 22:24:18 +0000 Subject: [PATCH 04/16] fix(deps): update rust crate clap to v4.5.23 (#3204) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b8f777c31..6f4dfa7595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,9 +932,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -942,9 +942,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -966,9 +966,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" From e9e303ee053d18890ead956d50faebf5b1df0e35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:14:10 +0000 Subject: [PATCH 05/16] fix(deps): update rust crate prost-reflect to v0.14.3 (#3205) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f4dfa7595..4fef4905dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4151,7 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.90", @@ -4172,9 +4172,9 @@ dependencies = [ [[package]] name = "prost-reflect" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7535b02f0e5efe3e1dbfcb428be152226ed0c66cad9541f2274c8ba8d4cd40" +checksum = "20ae544fca2892fd4b7e9ff26cba1090cedf1d4d95c2aded1af15d2f93f270b8" dependencies = [ "base64 0.22.1", "logos", @@ -4342,7 +4342,7 @@ dependencies = [ "bytes", "miette 7.4.0", "prost 0.13.4", - "prost-reflect 0.14.2", + "prost-reflect 0.14.3", "prost-types 0.13.3", "protox-parse 0.7.0", "thiserror 1.0.69", @@ -5660,7 +5660,7 @@ dependencies = [ "pretty_assertions", "prometheus", "prost 0.13.4", - "prost-reflect 0.14.2", + "prost-reflect 0.14.3", "protox 0.7.1", "protox-parse 0.7.0", "rand", From a261514ff624d9e150262ce06a2a6fb7598cb8f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Dec 2024 15:32:57 +0000 Subject: [PATCH 06/16] chore(deps): update rust crate async-graphql to v7.0.12 (#3207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fef4905dd..58db331d85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,9 +231,9 @@ dependencies = [ [[package]] name = "async-graphql" -version = "7.0.11" +version = "7.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba6d24703c5adc5ba9116901b92ee4e4c0643c01a56c4fd303f3818638d7449" +checksum = "10db7e8b2042f8d7ebcfebc482622411c23f88f3e9cd7fac74465b78fdab65f0" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -255,8 +255,7 @@ dependencies = [ "mime", "multer", "num-traits", - "once_cell", - "opentelemetry 0.25.0", + "opentelemetry 0.27.1", "pin-project-lite", "regex", "serde", @@ -269,9 +268,9 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "7.0.11" +version = "7.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94c2d176893486bd37cd1b6defadd999f7357bf5804e92f510c08bcf16c538f" +checksum = "ad560d871a344178c35568a15be1bbb40cbcaced57838bf2eb1f654802000df7" dependencies = [ "Inflector", "async-graphql-parser", @@ -319,9 +318,9 @@ dependencies = [ [[package]] name = "async-graphql-parser" -version = "7.0.11" +version = "7.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79272bdbf26af97866e149f05b2b546edb5c00e51b5f916289931ed233e208ad" +checksum = "1df338e3e6469f86cce1e2b0226644e9fd82ec04790e199f8dd06416632d89ea" dependencies = [ "async-graphql-value", "pest", @@ -331,9 +330,9 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "7.0.11" +version = "7.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5ec94176a12a8cbe985cd73f2e54dc9c702c88c766bdef12f1f3a67cedbee1" +checksum = "d4cffd8bb84bc7895672c4e9b71d21e35526ffd645a29aedeed165a3f4a7ba9b" dependencies = [ "bytes", "indexmap 2.6.0", @@ -1541,7 +1540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3580,14 +3579,13 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.25.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803801d3d3b71cd026851a53f974ea03df3d179cb758b260136a6c9e22e196af" +checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" dependencies = [ "futures-core", "futures-sink", "js-sys", - "once_cell", "pin-project-lite", "thiserror 1.0.69", ] @@ -4471,7 +4469,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5955,7 +5953,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From cd32933ec7ad5911fe6b8d992c76d56220b779bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Dec 2024 19:59:26 +0000 Subject: [PATCH 07/16] chore(deps): update dependency @cloudflare/workers-types to v4.20241205.0 (#3208) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tailcall-cloudflare/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tailcall-cloudflare/package-lock.json b/tailcall-cloudflare/package-lock.json index 96864730c8..9dfb159fdd 100644 --- a/tailcall-cloudflare/package-lock.json +++ b/tailcall-cloudflare/package-lock.json @@ -120,9 +120,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20241202.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20241202.0.tgz", - "integrity": "sha512-ts4JD6Wih62SDmlc+OcnN1Db/DgEBcl+BUpJr7ht7pgWP81PCLyPcomgDXIeAqt2NLiOIOMMkYQZ1ZtWDo3/8A==", + "version": "4.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20241205.0.tgz", + "integrity": "sha512-pj1VKRHT/ScQbHOIMFODZaNAlJHQHdBSZXNIdr9ebJzwBff9Qz8VdqhbhggV7f+aUEh8WSbrsPIo4a+WtgjUvw==", "dev": true }, "node_modules/@cspotcode/source-map-support": { @@ -2380,9 +2380,9 @@ } }, "@cloudflare/workers-types": { - "version": "4.20241202.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20241202.0.tgz", - "integrity": "sha512-ts4JD6Wih62SDmlc+OcnN1Db/DgEBcl+BUpJr7ht7pgWP81PCLyPcomgDXIeAqt2NLiOIOMMkYQZ1ZtWDo3/8A==", + "version": "4.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20241205.0.tgz", + "integrity": "sha512-pj1VKRHT/ScQbHOIMFODZaNAlJHQHdBSZXNIdr9ebJzwBff9Qz8VdqhbhggV7f+aUEh8WSbrsPIo4a+WtgjUvw==", "dev": true }, "@cspotcode/source-map-support": { From fc57e76056f91d62d3a1ddb343887a90fdd81e71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Dec 2024 23:17:00 +0000 Subject: [PATCH 08/16] chore(deps): update dependency miniflare to v3.20241205.0 (#3209) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tailcall-cloudflare/package-lock.json | 212 +++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 8 deletions(-) diff --git a/tailcall-cloudflare/package-lock.json b/tailcall-cloudflare/package-lock.json index 9dfb159fdd..0b490ce4cc 100644 --- a/tailcall-cloudflare/package-lock.json +++ b/tailcall-cloudflare/package-lock.json @@ -1230,9 +1230,9 @@ } }, "node_modules/miniflare": { - "version": "3.20241106.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241106.1.tgz", - "integrity": "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==", + "version": "3.20241205.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241205.0.tgz", + "integrity": "sha512-Z0cTtIf6ZrcAJ3SrOI9EUM3s4dkGhNeU6Ubl8sroYhsPVD+rtz3m5+p6McHFWCkcMff1o60X5XEKVTmkz0gbpA==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -1243,7 +1243,7 @@ "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", - "workerd": "1.20241106.1", + "workerd": "1.20241205.0", "ws": "^8.18.0", "youch": "^3.2.2", "zod": "^3.22.3" @@ -1255,6 +1255,106 @@ "node": ">=16.13" } }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", + "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", + "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", + "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", + "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", + "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/workerd": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", + "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20241205.0", + "@cloudflare/workerd-darwin-arm64": "1.20241205.0", + "@cloudflare/workerd-linux-64": "1.20241205.0", + "@cloudflare/workerd-linux-arm64": "1.20241205.0", + "@cloudflare/workerd-windows-64": "1.20241205.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2276,6 +2376,32 @@ "@esbuild/win32-x64": "0.17.19" } }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "3.20241106.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241106.1.tgz", + "integrity": "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "^8.8.0", + "acorn-walk": "^8.2.0", + "capnp-ts": "^0.7.0", + "exit-hook": "^2.2.1", + "glob-to-regexp": "^0.4.1", + "stoppable": "^1.1.0", + "undici": "^5.28.4", + "workerd": "1.20241106.1", + "ws": "^8.18.0", + "youch": "^3.2.2", + "zod": "^3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -3057,9 +3183,9 @@ "dev": true }, "miniflare": { - "version": "3.20241106.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241106.1.tgz", - "integrity": "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==", + "version": "3.20241205.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241205.0.tgz", + "integrity": "sha512-Z0cTtIf6ZrcAJ3SrOI9EUM3s4dkGhNeU6Ubl8sroYhsPVD+rtz3m5+p6McHFWCkcMff1o60X5XEKVTmkz0gbpA==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.8.1", @@ -3070,10 +3196,60 @@ "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", - "workerd": "1.20241106.1", + "workerd": "1.20241205.0", "ws": "^8.18.0", "youch": "^3.2.2", "zod": "^3.22.3" + }, + "dependencies": { + "@cloudflare/workerd-darwin-64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", + "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", + "dev": true, + "optional": true + }, + "@cloudflare/workerd-darwin-arm64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", + "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", + "dev": true, + "optional": true + }, + "@cloudflare/workerd-linux-64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", + "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", + "dev": true, + "optional": true + }, + "@cloudflare/workerd-linux-arm64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", + "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", + "dev": true, + "optional": true + }, + "@cloudflare/workerd-windows-64": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", + "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", + "dev": true, + "optional": true + }, + "workerd": { + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", + "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", + "dev": true, + "requires": { + "@cloudflare/workerd-darwin-64": "1.20241205.0", + "@cloudflare/workerd-darwin-arm64": "1.20241205.0", + "@cloudflare/workerd-linux-64": "1.20241205.0", + "@cloudflare/workerd-linux-arm64": "1.20241205.0", + "@cloudflare/workerd-windows-64": "1.20241205.0" + } + } } }, "ms": { @@ -3679,6 +3855,26 @@ "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" } + }, + "miniflare": { + "version": "3.20241106.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241106.1.tgz", + "integrity": "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "^8.8.0", + "acorn-walk": "^8.2.0", + "capnp-ts": "^0.7.0", + "exit-hook": "^2.2.1", + "glob-to-regexp": "^0.4.1", + "stoppable": "^1.1.0", + "undici": "^5.28.4", + "workerd": "1.20241106.1", + "ws": "^8.18.0", + "youch": "^3.2.2", + "zod": "^3.22.3" + } } } }, From e804dbbc0792f198bdbfedac6ce6a72be876c660 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 01:11:22 +0000 Subject: [PATCH 09/16] chore(deps): update rust crate datatest-stable to v0.2.10 (#3210) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 65 ++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58db331d85..4cd251c85f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,7 +662,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -671,6 +680,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -1291,9 +1306,9 @@ dependencies = [ [[package]] name = "datatest-stable" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a560b3fd20463b56397bd457aa71243ccfdcffe696050b66e3b1e0ec0457e7f1" +checksum = "833306ca7eec4d95844e65f0d7502db43888c5c1006c6c517e8cf51a27d15431" dependencies = [ "camino", "fancy-regex", @@ -1540,7 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1605,11 +1620,11 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -2815,7 +2830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.5.3", "ena", "itertools 0.11.0", "lalrpop-util", @@ -2980,7 +2995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3005,14 +3020,14 @@ dependencies = [ [[package]] name = "libtest-mimic" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" +checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" dependencies = [ + "anstream", + "anstyle", "clap", "escape8259", - "termcolor", - "threadpool", ] [[package]] @@ -4469,7 +4484,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5953,7 +5968,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5967,15 +5982,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "test-log" version = "0.2.16" @@ -6048,15 +6054,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "time" version = "0.3.36" @@ -6920,7 +6917,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] From f9ef23967ef633aabba320e8928819d977326887 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 04:41:27 +0000 Subject: [PATCH 10/16] chore(deps): update dependency wrangler to v3.93.0 (#3211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tailcall-cloudflare/package-lock.json | 359 ++++++-------------------- 1 file changed, 73 insertions(+), 286 deletions(-) diff --git a/tailcall-cloudflare/package-lock.json b/tailcall-cloudflare/package-lock.json index 0b490ce4cc..c9d1fc5ce7 100644 --- a/tailcall-cloudflare/package-lock.json +++ b/tailcall-cloudflare/package-lock.json @@ -27,9 +27,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241106.1.tgz", - "integrity": "sha512-zxvaToi1m0qzAScrxFt7UvFVqU8DxrCO2CinM1yQkv5no7pA1HolpIrwZ0xOhR3ny64Is2s/J6BrRjpO5dM9Zw==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", + "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", "cpu": [ "x64" ], @@ -43,9 +43,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241106.1.tgz", - "integrity": "sha512-j3dg/42D/bPgfNP3cRUBxF+4waCKO/5YKwXNj+lnVOwHxDu+ne5pFw9TIkKYcWTcwn0ZUkbNZNM5rhJqRn4xbg==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", + "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", "cpu": [ "arm64" ], @@ -59,9 +59,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241106.1.tgz", - "integrity": "sha512-Ih+Ye8E1DMBXcKrJktGfGztFqHKaX1CeByqshmTbODnWKHt6O65ax3oTecUwyC0+abuyraOpAtdhHNpFMhUkmw==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", + "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", "cpu": [ "x64" ], @@ -75,9 +75,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241106.1.tgz", - "integrity": "sha512-mdQFPk4+14Yywn7n1xIzI+6olWM8Ybz10R7H3h+rk0XulMumCWUCy1CzIDauOx6GyIcSgKIibYMssVHZR30ObA==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", + "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", "cpu": [ "arm64" ], @@ -91,9 +91,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241106.1.tgz", - "integrity": "sha512-4rtcss31E/Rb/PeFocZfr+B9i1MdrkhsTBWizh8siNR4KMmkslU2xs2wPaH1z8+ErxkOsHrKRa5EPLh5rIiFeg==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", + "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", "cpu": [ "x64" ], @@ -107,9 +107,9 @@ } }, "node_modules/@cloudflare/workers-shared": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.9.0.tgz", - "integrity": "sha512-eP6Ir45uPbKnpADVzUCtkRUYxYxjB1Ew6n/whTJvHu8H4m93USHAceCMm736VBZdlxuhXXUjEP3fCUxKPn+cfw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.10.0.tgz", + "integrity": "sha512-j3EwZBc9ctavmFVOQT1gqztRO/Plx4ZR0LMEEOif+5YoCcuD1P7/NEjlODPMc5a1w+8+7A/H+Ci8Ihd55+x0Zw==", "dev": true, "dependencies": { "mime": "^3.0.0", @@ -1255,106 +1255,6 @@ "node": ">=16.13" } }, - "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", - "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", - "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", - "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", - "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/miniflare/node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", - "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/miniflare/node_modules/workerd": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", - "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20241205.0", - "@cloudflare/workerd-darwin-arm64": "1.20241205.0", - "@cloudflare/workerd-linux-64": "1.20241205.0", - "@cloudflare/workerd-linux-arm64": "1.20241205.0", - "@cloudflare/workerd-windows-64": "1.20241205.0" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1500,15 +1400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/rollup": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", @@ -1753,9 +1644,9 @@ }, "node_modules/unenv": { "name": "unenv-nightly", - "version": "2.0.0-20241121-161142-806b5c0", - "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-20241121-161142-806b5c0.tgz", - "integrity": "sha512-RnFOasE/O0Q55gBkNB1b84OgKttgLEijGO0JCWpbn+O4XxpyCQg89NmcqQ5RGUiy4y+rMIrKzePTquQcLQF5pQ==", + "version": "2.0.0-20241204-140205-a5d5190", + "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-20241204-140205-a5d5190.tgz", + "integrity": "sha512-jpmAytLeiiW01pl5bhVn9wYJ4vtiLdhGe10oXlJBuQEX8mxjxO8BlEXGHU4vr4yEikjFP1wsomTHt/CLU8kUwg==", "dev": true, "dependencies": { "defu": "^6.1.4", @@ -1923,9 +1814,9 @@ } }, "node_modules/workerd": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241106.1.tgz", - "integrity": "sha512-1GdKl0kDw8rrirr/ThcK66Kbl4/jd4h8uHx5g7YHBrnenY5SX1UPuop2cnCzYUxlg55kPjzIqqYslz1muRFgFw==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", + "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", "dev": true, "hasInstallScript": true, "bin": { @@ -1935,21 +1826,21 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20241106.1", - "@cloudflare/workerd-darwin-arm64": "1.20241106.1", - "@cloudflare/workerd-linux-64": "1.20241106.1", - "@cloudflare/workerd-linux-arm64": "1.20241106.1", - "@cloudflare/workerd-windows-64": "1.20241106.1" + "@cloudflare/workerd-darwin-64": "1.20241205.0", + "@cloudflare/workerd-darwin-arm64": "1.20241205.0", + "@cloudflare/workerd-linux-64": "1.20241205.0", + "@cloudflare/workerd-linux-arm64": "1.20241205.0", + "@cloudflare/workerd-windows-64": "1.20241205.0" } }, "node_modules/wrangler": { - "version": "3.91.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.91.0.tgz", - "integrity": "sha512-Hdzn6wbY9cz5kL85ZUvWLwLIH7nPaEVRblfms40jhRf4qQO/Zf74aFlku8rQFbe8/2aVZFaxJVfBd6JQMeMSBQ==", + "version": "3.93.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.93.0.tgz", + "integrity": "sha512-+wfxjOrtm6YgDS+NdJkB6aiBIS3ED97mNRQmfrEShRJW4pVo4sWY6oQ1FsGT+j4tGHplrTbWCE6U5yTgjNW/lw==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/workers-shared": "0.9.0", + "@cloudflare/workers-shared": "0.10.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", @@ -1957,15 +1848,14 @@ "date-fns": "^4.1.0", "esbuild": "0.17.19", "itty-time": "^1.0.6", - "miniflare": "3.20241106.1", + "miniflare": "3.20241205.0", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", - "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", - "unenv": "npm:unenv-nightly@2.0.0-20241121-161142-806b5c0", - "workerd": "1.20241106.1", + "unenv": "npm:unenv-nightly@2.0.0-20241204-140205-a5d5190", + "workerd": "1.20241205.0", "xxhash-wasm": "^1.0.1" }, "bin": { @@ -1979,7 +1869,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20241106.0" + "@cloudflare/workers-types": "^4.20241205.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -2376,32 +2266,6 @@ "@esbuild/win32-x64": "0.17.19" } }, - "node_modules/wrangler/node_modules/miniflare": { - "version": "3.20241106.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241106.1.tgz", - "integrity": "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "^8.8.0", - "acorn-walk": "^8.2.0", - "capnp-ts": "^0.7.0", - "exit-hook": "^2.2.1", - "glob-to-regexp": "^0.4.1", - "stoppable": "^1.1.0", - "undici": "^5.28.4", - "workerd": "1.20241106.1", - "ws": "^8.18.0", - "youch": "^3.2.2", - "zod": "^3.22.3" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=16.13" - } - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -2461,44 +2325,44 @@ } }, "@cloudflare/workerd-darwin-64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241106.1.tgz", - "integrity": "sha512-zxvaToi1m0qzAScrxFt7UvFVqU8DxrCO2CinM1yQkv5no7pA1HolpIrwZ0xOhR3ny64Is2s/J6BrRjpO5dM9Zw==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", + "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", "dev": true, "optional": true }, "@cloudflare/workerd-darwin-arm64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241106.1.tgz", - "integrity": "sha512-j3dg/42D/bPgfNP3cRUBxF+4waCKO/5YKwXNj+lnVOwHxDu+ne5pFw9TIkKYcWTcwn0ZUkbNZNM5rhJqRn4xbg==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", + "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241106.1.tgz", - "integrity": "sha512-Ih+Ye8E1DMBXcKrJktGfGztFqHKaX1CeByqshmTbODnWKHt6O65ax3oTecUwyC0+abuyraOpAtdhHNpFMhUkmw==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", + "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-arm64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241106.1.tgz", - "integrity": "sha512-mdQFPk4+14Yywn7n1xIzI+6olWM8Ybz10R7H3h+rk0XulMumCWUCy1CzIDauOx6GyIcSgKIibYMssVHZR30ObA==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", + "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", "dev": true, "optional": true }, "@cloudflare/workerd-windows-64": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241106.1.tgz", - "integrity": "sha512-4rtcss31E/Rb/PeFocZfr+B9i1MdrkhsTBWizh8siNR4KMmkslU2xs2wPaH1z8+ErxkOsHrKRa5EPLh5rIiFeg==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", + "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", "dev": true, "optional": true }, "@cloudflare/workers-shared": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.9.0.tgz", - "integrity": "sha512-eP6Ir45uPbKnpADVzUCtkRUYxYxjB1Ew6n/whTJvHu8H4m93USHAceCMm736VBZdlxuhXXUjEP3fCUxKPn+cfw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.10.0.tgz", + "integrity": "sha512-j3EwZBc9ctavmFVOQT1gqztRO/Plx4ZR0LMEEOif+5YoCcuD1P7/NEjlODPMc5a1w+8+7A/H+Ci8Ihd55+x0Zw==", "dev": true, "requires": { "mime": "^3.0.0", @@ -3200,56 +3064,6 @@ "ws": "^8.18.0", "youch": "^3.2.2", "zod": "^3.22.3" - }, - "dependencies": { - "@cloudflare/workerd-darwin-64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", - "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", - "dev": true, - "optional": true - }, - "@cloudflare/workerd-darwin-arm64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", - "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", - "dev": true, - "optional": true - }, - "@cloudflare/workerd-linux-64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", - "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", - "dev": true, - "optional": true - }, - "@cloudflare/workerd-linux-arm64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", - "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", - "dev": true, - "optional": true - }, - "@cloudflare/workerd-windows-64": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", - "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", - "dev": true, - "optional": true - }, - "workerd": { - "version": "1.20241205.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", - "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", - "dev": true, - "requires": { - "@cloudflare/workerd-darwin-64": "1.20241205.0", - "@cloudflare/workerd-darwin-arm64": "1.20241205.0", - "@cloudflare/workerd-linux-64": "1.20241205.0", - "@cloudflare/workerd-linux-arm64": "1.20241205.0", - "@cloudflare/workerd-windows-64": "1.20241205.0" - } - } } }, "ms": { @@ -3346,12 +3160,6 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, - "resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true - }, "rollup": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", @@ -3557,9 +3365,9 @@ "dev": true }, "unenv": { - "version": "npm:unenv-nightly@2.0.0-20241121-161142-806b5c0", - "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-20241121-161142-806b5c0.tgz", - "integrity": "sha512-RnFOasE/O0Q55gBkNB1b84OgKttgLEijGO0JCWpbn+O4XxpyCQg89NmcqQ5RGUiy4y+rMIrKzePTquQcLQF5pQ==", + "version": "npm:unenv-nightly@2.0.0-20241204-140205-a5d5190", + "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-20241204-140205-a5d5190.tgz", + "integrity": "sha512-jpmAytLeiiW01pl5bhVn9wYJ4vtiLdhGe10oXlJBuQEX8mxjxO8BlEXGHU4vr4yEikjFP1wsomTHt/CLU8kUwg==", "dev": true, "requires": { "defu": "^6.1.4", @@ -3632,26 +3440,26 @@ } }, "workerd": { - "version": "1.20241106.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241106.1.tgz", - "integrity": "sha512-1GdKl0kDw8rrirr/ThcK66Kbl4/jd4h8uHx5g7YHBrnenY5SX1UPuop2cnCzYUxlg55kPjzIqqYslz1muRFgFw==", + "version": "1.20241205.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", + "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", "dev": true, "requires": { - "@cloudflare/workerd-darwin-64": "1.20241106.1", - "@cloudflare/workerd-darwin-arm64": "1.20241106.1", - "@cloudflare/workerd-linux-64": "1.20241106.1", - "@cloudflare/workerd-linux-arm64": "1.20241106.1", - "@cloudflare/workerd-windows-64": "1.20241106.1" + "@cloudflare/workerd-darwin-64": "1.20241205.0", + "@cloudflare/workerd-darwin-arm64": "1.20241205.0", + "@cloudflare/workerd-linux-64": "1.20241205.0", + "@cloudflare/workerd-linux-arm64": "1.20241205.0", + "@cloudflare/workerd-windows-64": "1.20241205.0" } }, "wrangler": { - "version": "3.91.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.91.0.tgz", - "integrity": "sha512-Hdzn6wbY9cz5kL85ZUvWLwLIH7nPaEVRblfms40jhRf4qQO/Zf74aFlku8rQFbe8/2aVZFaxJVfBd6JQMeMSBQ==", + "version": "3.93.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.93.0.tgz", + "integrity": "sha512-+wfxjOrtm6YgDS+NdJkB6aiBIS3ED97mNRQmfrEShRJW4pVo4sWY6oQ1FsGT+j4tGHplrTbWCE6U5yTgjNW/lw==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/workers-shared": "0.9.0", + "@cloudflare/workers-shared": "0.10.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", @@ -3660,15 +3468,14 @@ "esbuild": "0.17.19", "fsevents": "~2.3.2", "itty-time": "^1.0.6", - "miniflare": "3.20241106.1", + "miniflare": "3.20241205.0", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", - "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", - "unenv": "npm:unenv-nightly@2.0.0-20241121-161142-806b5c0", - "workerd": "1.20241106.1", + "unenv": "npm:unenv-nightly@2.0.0-20241204-140205-a5d5190", + "workerd": "1.20241205.0", "xxhash-wasm": "^1.0.1" }, "dependencies": { @@ -3855,26 +3662,6 @@ "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" } - }, - "miniflare": { - "version": "3.20241106.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241106.1.tgz", - "integrity": "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "^8.8.0", - "acorn-walk": "^8.2.0", - "capnp-ts": "^0.7.0", - "exit-hook": "^2.2.1", - "glob-to-regexp": "^0.4.1", - "stoppable": "^1.1.0", - "undici": "^5.28.4", - "workerd": "1.20241106.1", - "ws": "^8.18.0", - "youch": "^3.2.2", - "zod": "^3.22.3" - } } } }, From 08fbc987af1b128b667ae68c6e3afa657cbea223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 06:05:03 +0000 Subject: [PATCH 11/16] chore(deps): update rust crate indexmap to v2.7.0 (#3212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cd251c85f..c636969abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,7 +250,7 @@ dependencies = [ "futures-util", "handlebars", "http 1.1.0", - "indexmap 2.6.0", + "indexmap 2.7.0", "lru", "mime", "multer", @@ -335,7 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4cffd8bb84bc7895672c4e9b71d21e35526ffd645a29aedeed165a3f4a7ba9b" dependencies = [ "bytes", - "indexmap 2.6.0", + "indexmap 2.7.0", "serde", "serde_json", ] @@ -2066,7 +2066,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.6.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -2685,9 +2685,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3873,7 +3873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.6.0", + "indexmap 2.7.0", ] [[package]] @@ -4256,7 +4256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257" dependencies = [ "anyhow", - "indexmap 2.6.0", + "indexmap 2.7.0", "log", "protobuf 3.7.1", "protobuf-support", @@ -4821,7 +4821,7 @@ dependencies = [ "convert_case", "fnv", "ident_case", - "indexmap 2.6.0", + "indexmap 2.7.0", "proc-macro-crate 1.3.1", "proc-macro2", "quote", @@ -5169,7 +5169,7 @@ version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "itoa", "memchr", "ryu", @@ -5234,7 +5234,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "itoa", "ryu", "serde", @@ -5642,7 +5642,7 @@ dependencies = [ "hyper 0.14.31", "hyper-rustls 0.25.0", "indenter", - "indexmap 2.6.0", + "indexmap 2.7.0", "inquire", "insta", "jsonwebtoken", @@ -6249,7 +6249,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "toml_datetime", "winnow 0.5.40", ] @@ -6260,7 +6260,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "toml_datetime", "winnow 0.6.20", ] From fe220beb512040287b5d3926a86b70fe40cf169f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 06:37:58 +0000 Subject: [PATCH 12/16] fix(deps): update dependency type-fest to v4.30.0 (#3214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- npm/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npm/package-lock.json b/npm/package-lock.json index 7ba67ca9da..f8ba2ce98b 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -863,9 +863,9 @@ } }, "node_modules/type-fest": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", - "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", + "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" From 858ac97a7bf7c2cfaae9c58fadef5a8d187a16ed Mon Sep 17 00:00:00 2001 From: DLillard0 <53611763+DLillard0@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:15:51 +0800 Subject: [PATCH 13/16] feat(gen): support proto_paths for gen graphql through proto file (#3206) --- src/cli/generator/config.rs | 18 ++++- src/cli/generator/generator.rs | 6 +- src/core/config/reader.rs | 2 +- src/core/proto_reader/reader.rs | 60 +++++++++++--- .../fixtures/protobuf/news_proto_paths.proto | 15 ++++ .../gen_proto_with_proto_paths_config.md | 31 +++++++ ..._gen_proto_with_proto_paths_config.md.snap | 81 +++++++++++++++++++ 7 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto create mode 100644 tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md create mode 100644 tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_proto_with_proto_paths_config.md.snap diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index 26a077906b..1e6e8f119c 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -81,10 +81,13 @@ pub enum Source { is_mutation: Option, field_name: String, }, + #[serde(rename_all = "camelCase")] Proto { src: Location, url: String, #[serde(skip_serializing_if = "Option::is_none")] + proto_paths: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "connectRPC")] connect_rpc: Option, }, @@ -220,9 +223,20 @@ impl Source { is_mutation, }) } - Source::Proto { src, url, connect_rpc } => { + Source::Proto { src, url, proto_paths, connect_rpc } => { let resolved_path = src.into_resolved(parent_dir); - Ok(Source::Proto { src: resolved_path, url, connect_rpc }) + let resolved_proto_paths = proto_paths.map(|paths| { + paths + .into_iter() + .map(|path| path.into_resolved(parent_dir)) + .collect() + }); + Ok(Source::Proto { + src: resolved_path, + url, + proto_paths: resolved_proto_paths, + connect_rpc, + }) } Source::Config { src } => { let resolved_path = src.into_resolved(parent_dir); diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index b56e162808..94ef7319cd 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -135,9 +135,11 @@ impl Generator { headers: headers.into_btree_map(), }); } - Source::Proto { src, url, connect_rpc } => { + Source::Proto { src, url, proto_paths, connect_rpc } => { let path = src.0; - let mut metadata = proto_reader.read(&path).await?; + let proto_paths = + proto_paths.map(|paths| paths.into_iter().map(|l| l.0).collect::>()); + let mut metadata = proto_reader.read(&path, proto_paths.as_deref()).await?; if let Some(relative_path_to_proto) = to_relative_path(output_dir, &path) { metadata.path = relative_path_to_proto; } diff --git a/src/core/config/reader.rs b/src/core/config/reader.rs index 609cb0eb62..f6d539360f 100644 --- a/src/core/config/reader.rs +++ b/src/core/config/reader.rs @@ -87,7 +87,7 @@ impl ConfigReader { } } LinkType::Protobuf => { - let meta = self.proto_reader.read(path).await?; + let meta = self.proto_reader.read(path, None).await?; extensions.add_proto(meta); } LinkType::Script => { diff --git a/src/core/proto_reader/reader.rs b/src/core/proto_reader/reader.rs index 81c65cc16a..6fdd0cd476 100644 --- a/src/core/proto_reader/reader.rs +++ b/src/core/proto_reader/reader.rs @@ -65,7 +65,7 @@ impl ProtoReader { /// Asynchronously reads all proto files from a list of paths pub async fn read_all>(&self, paths: &[T]) -> anyhow::Result> { - let resolved_protos = join_all(paths.iter().map(|v| self.read(v.as_ref()))) + let resolved_protos = join_all(paths.iter().map(|v| self.read(v.as_ref(), None))) .await .into_iter() .collect::>>()?; @@ -73,12 +73,20 @@ impl ProtoReader { } /// Reads a proto file from a path - pub async fn read>(&self, path: T) -> anyhow::Result { - let file_read = self.read_proto(path.as_ref(), None).await?; + pub async fn read>( + &self, + path: T, + proto_paths: Option<&[String]>, + ) -> anyhow::Result { + let file_read = self.read_proto(path.as_ref(), None, None).await?; Self::check_package(&file_read)?; let descriptors = self - .file_resolve(file_read, PathBuf::from(path.as_ref()).parent()) + .file_resolve( + file_read, + PathBuf::from(path.as_ref()).parent(), + proto_paths, + ) .await?; let metadata = ProtoMetadata { descriptor_set: FileDescriptorSet { file: descriptors }, @@ -130,12 +138,22 @@ impl ProtoReader { &self, parent_proto: FileDescriptorProto, parent_path: Option<&Path>, + proto_paths: Option<&[String]>, ) -> anyhow::Result> { self.resolve_dependencies(parent_proto, |import| { let parent_path = parent_path.map(|p| p.to_path_buf()); let this = self.clone(); - - async move { this.read_proto(import, parent_path.as_deref()).await }.boxed() + let proto_paths = proto_paths.map(|paths| { + paths + .iter() + .map(|p| Path::new(p).to_path_buf()) + .collect::>() + }); + async move { + this.read_proto(import, parent_path.as_deref(), proto_paths.as_deref()) + .await + } + .boxed() }) .await } @@ -159,27 +177,39 @@ impl ProtoReader { &self, path: T, parent_dir: Option<&Path>, + proto_paths: Option<&[PathBuf]>, ) -> anyhow::Result { let content = if let Ok(file) = GoogleFileResolver::new().open_file(path.as_ref()) { file.source() .context("Unable to extract content of google well-known proto file")? .to_string() } else { - let path = Self::resolve_path(path.as_ref(), parent_dir); + let path = Self::resolve_path(path.as_ref(), parent_dir, proto_paths); self.reader.read_file(path).await?.content }; Ok(protox_parse::parse(path.as_ref(), &content)?) } /// Checks if path is absolute else it joins file path with relative dir /// path - fn resolve_path(src: &str, root_dir: Option<&Path>) -> String { + fn resolve_path(src: &str, root_dir: Option<&Path>, proto_paths: Option<&[PathBuf]>) -> String { if src.starts_with("http") { return src.to_string(); } if Path::new(&src).is_absolute() { - src.to_string() - } else if let Some(path) = root_dir { + return src.to_string(); + } + + if let Some(proto_paths) = proto_paths { + for proto_path in proto_paths { + let path = proto_path.join(src); + if path.exists() { + return path.to_string_lossy().to_string(); + } + } + } + + if let Some(path) = root_dir { path.join(src).to_string_lossy().to_string() } else { src.to_string() @@ -210,7 +240,7 @@ mod test_proto_config { let runtime = crate::core::runtime::test::init(None); let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); reader - .read_proto("google/protobuf/empty.proto", None) + .read_proto("google/protobuf/empty.proto", None, None) .await .unwrap(); } @@ -225,7 +255,11 @@ mod test_proto_config { let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); let file_descriptors = reader - .file_resolve(reader.read_proto(&test_file, None).await?, Some(test_dir)) + .file_resolve( + reader.read_proto(&test_file, None, None).await?, + Some(test_dir), + None, + ) .await?; for file in file_descriptors .iter() @@ -248,7 +282,7 @@ mod test_proto_config { let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); let proto_no_pkg = PathBuf::from(tailcall_fixtures::configs::SELF).join("proto_no_pkg.graphql"); - let config_module = reader.read(proto_no_pkg.to_str().unwrap()).await; + let config_module = reader.read(proto_no_pkg.to_str().unwrap(), None).await; assert!(config_module.is_err()); Ok(()) } diff --git a/tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto b/tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto new file mode 100644 index 0000000000..e4d9b6c72b --- /dev/null +++ b/tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package news; + +import "protobuf/news_dto.proto"; +import "google/protobuf/empty.proto"; + +service NewsService { + rpc GetAllNews(google.protobuf.Empty) returns (NewsList) {} + rpc GetNews(NewsId) returns (News) {} + rpc GetMultipleNews(MultipleNewsId) returns (NewsList) {} + rpc DeleteNews(NewsId) returns (google.protobuf.Empty) {} + rpc EditNews(News) returns (News) {} + rpc AddNews(News) returns (News) {} +} \ No newline at end of file diff --git a/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md b/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md new file mode 100644 index 0000000000..618da86ab5 --- /dev/null +++ b/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md @@ -0,0 +1,31 @@ +```json @config +{ + "inputs": [ + { + "curl": { + "src": "http://jsonplaceholder.typicode.com/users", + "fieldName": "users" + } + }, + { + "proto": { + "src": "tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto", + "url": "http://localhost:50051", + "protoPaths": ["tailcall-fixtures/fixtures/"] + } + } + ], + "preset": { + "mergeType": 1.0, + "inferTypeNames": true, + "treeShake": true + }, + "output": { + "path": "./output.graphql", + "format": "graphQL" + }, + "schema": { + "query": "Query" + } +} +``` diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_proto_with_proto_paths_config.md.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_proto_with_proto_paths_config.md.snap new file mode 100644 index 0000000000..3e176c6dfd --- /dev/null +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_proto_with_proto_paths_config.md.snap @@ -0,0 +1,81 @@ +--- +source: tests/cli/gen.rs +expression: config.to_sdl() +--- +schema @server @upstream { + query: Query +} + +input GEN__news__MultipleNewsId { + ids: [Id] +} + +input GEN__news__NewsInput { + body: String + id: Int + postImage: String + status: Status + title: String +} + +input Id { + id: Int +} + +enum Status { + DELETED + DRAFT + PUBLISHED +} + +type Address { + city: String + geo: Geo + street: String + suite: String + zipcode: String +} + +type Company { + bs: String + catchPhrase: String + name: String +} + +type GEN__news__NewsList { + news: [News] +} + +type Geo { + lat: String + lng: String +} + +type News { + body: String + id: Int + postImage: String + status: Status + title: String +} + +type Query { + GEN__news__NewsService__AddNews(news: GEN__news__NewsInput!): News @grpc(url: "http://localhost:50051", body: "{{.args.news}}", method: "news.NewsService.AddNews") + GEN__news__NewsService__DeleteNews(newsId: Id!): Empty @grpc(url: "http://localhost:50051", body: "{{.args.newsId}}", method: "news.NewsService.DeleteNews") + GEN__news__NewsService__EditNews(news: GEN__news__NewsInput!): News @grpc(url: "http://localhost:50051", body: "{{.args.news}}", method: "news.NewsService.EditNews") + GEN__news__NewsService__GetAllNews: GEN__news__NewsList @grpc(url: "http://localhost:50051", method: "news.NewsService.GetAllNews") + GEN__news__NewsService__GetMultipleNews(multipleNewsId: GEN__news__MultipleNewsId!): GEN__news__NewsList @grpc(url: "http://localhost:50051", body: "{{.args.multipleNewsId}}", method: "news.NewsService.GetMultipleNews") + GEN__news__NewsService__GetNews(newsId: Id!): News @grpc(url: "http://localhost:50051", body: "{{.args.newsId}}", method: "news.NewsService.GetNews") + users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") +} + +type User { + address: Address + company: Company + email: String + id: Int + name: String + phone: String + username: String + website: String +} From 01ebf87f660a5fd9c0dab614baa5b289befa65c0 Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:00:01 +0100 Subject: [PATCH 14/16] feat: drop support for `json` `yaml` from `Config` (#3201) Co-authored-by: laststylebender <43403528+laststylebender14@users.noreply.github.com> Co-authored-by: blacksmith-sh[bot] <157653362+blacksmith-sh[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Tushar Mathur --- .graphqlrc.yml | 2 - examples/jsonplaceholder.json | 119 -- examples/jsonplaceholder.yml | 73 -- examples/jsonplaceholder_batch.json | 113 -- examples/jsonplaceholder_batch.yml | 71 -- generated/.tailcallrc.schema.json | 1056 +---------------- src/core/config/config.rs | 86 +- src/core/config/reader.rs | 53 +- .../fixtures/nested-unions-recursive.graphql | 32 + .../fixtures/nested-unions.graphql | 32 + .../fixtures/recursive-input.graphql | 17 + .../fixtures/union-in-type.graphql | 33 + .../config/transformer/fixtures/union.graphql | 22 + src/core/config/transformer/nested_unions.rs | 11 +- .../config/transformer/union_input_type.rs | 17 +- .../configs/yaml-nested-unions-recursive.yaml | 56 - .../fixtures/configs/yaml-nested-unions.yaml | 56 - .../configs/yaml-recursive-input.yaml | 34 - .../fixtures/configs/yaml-union-in-type.yaml | 64 - .../fixtures/configs/yaml-union.yaml | 40 - .../snapshots/test-enum-empty.md_error.snap | 3 +- tests/execution/batching-disabled.md | 74 +- tests/execution/batching.md | 54 +- tests/execution/cache-control.md | 70 +- tests/execution/custom-headers.md | 43 +- tests/execution/env-value.md | 86 +- tests/execution/https.md | 61 - tests/execution/recursive-types-json.md | 211 ---- tests/execution/ref-other-nested.md | 84 +- .../execution/request-to-upstream-batching.md | 80 +- tests/execution/simple-query.md | 51 +- tests/execution/test-enum-empty.md | 46 +- tests/execution/test-interface-from-json.md | 47 - tests/execution/test-static-value.md | 62 - tests/execution/upstream-batching.md | 77 +- tests/execution/with-args-url.md | 59 +- tests/execution/yaml-nested-unions.md | 86 +- tests/execution/yaml-union-in-type.md | 87 +- tests/execution/yaml-union.md | 78 +- tests/graphql_spec.graphql | 1 - 40 files changed, 425 insertions(+), 2922 deletions(-) delete mode 100644 examples/jsonplaceholder.json delete mode 100644 examples/jsonplaceholder.yml delete mode 100644 examples/jsonplaceholder_batch.json delete mode 100644 examples/jsonplaceholder_batch.yml create mode 100644 src/core/config/transformer/fixtures/nested-unions-recursive.graphql create mode 100644 src/core/config/transformer/fixtures/nested-unions.graphql create mode 100644 src/core/config/transformer/fixtures/recursive-input.graphql create mode 100644 src/core/config/transformer/fixtures/union-in-type.graphql create mode 100644 src/core/config/transformer/fixtures/union.graphql delete mode 100644 tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml delete mode 100644 tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml delete mode 100644 tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml delete mode 100644 tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml delete mode 100644 tailcall-fixtures/fixtures/configs/yaml-union.yaml delete mode 100644 tests/execution/https.md delete mode 100644 tests/execution/recursive-types-json.md delete mode 100644 tests/execution/test-interface-from-json.md delete mode 100644 tests/execution/test-static-value.md delete mode 100644 tests/graphql_spec.graphql diff --git a/.graphqlrc.yml b/.graphqlrc.yml index 1e8b8bf1c2..8e19f17adb 100644 --- a/.graphqlrc.yml +++ b/.graphqlrc.yml @@ -1,5 +1,3 @@ schema: - "./examples/jsonplaceholder.graphql" - "./generated/.tailcallrc.graphql" - # for tests inside the repo - - "./tests/graphql_spec.graphql" diff --git a/examples/jsonplaceholder.json b/examples/jsonplaceholder.json deleted file mode 100644 index 802964d591..0000000000 --- a/examples/jsonplaceholder.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "$schema": "../generated/.tailcallrc.schema.json", - "server": { - "hostname": "0.0.0.0", - "port": 8000 - }, - "upstream": { - "httpCache": 42 - }, - "schema": { - "query": "Query" - }, - "types": { - "Post": { - "fields": { - "body": { - "type": { - "name": "String", - "required": true - } - }, - "id": { - "type": { - "name": "Int", - "required": true - } - }, - "title": { - "type": { - "name": "String", - "required": true - } - }, - "user": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/{{value.userId}}" - } - }, - "userId": { - "type": { - "name": "Int", - "required": true - } - } - } - }, - "Query": { - "fields": { - "posts": { - "type": { - "list": { - "name": "Post" - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/posts" - } - }, - "user": { - "type": { - "name": "User" - }, - "args": { - "id": { - "type": { - "name": "Int", - "required": true - } - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/{{args.id}}" - } - } - } - }, - "User": { - "fields": { - "email": { - "type": { - "name": "String", - "required": true - } - }, - "id": { - "type": { - "name": "Int", - "required": true - } - }, - "name": { - "type": { - "name": "String", - "required": true - } - }, - "phone": { - "type": { - "name": "String" - } - }, - "username": { - "type": { - "name": "String", - "required": true - } - }, - "website": { - "type": { - "name": "String" - } - } - } - } - } -} diff --git a/examples/jsonplaceholder.yml b/examples/jsonplaceholder.yml deleted file mode 100644 index 79c94fb96a..0000000000 --- a/examples/jsonplaceholder.yml +++ /dev/null @@ -1,73 +0,0 @@ -server: - hostname: 0.0.0.0 - port: 8000 -upstream: - httpCache: 42 -schema: - query: Query -types: - Post: - fields: - body: - type: - name: String - required: true - id: - type: - name: Int - required: true - title: - type: - name: String - required: true - user: - type: - name: User - http: - url: http://jsonplaceholder.typicode.com/users/{{value.userId}} - userId: - type: - name: Int - required: true - Query: - fields: - posts: - type: - list: - name: Post - http: - url: http://jsonplaceholder.typicode.com/posts - user: - type: - name: User - args: - id: - type: - name: Int - required: true - http: - url: http://jsonplaceholder.typicode.com/users/{{args.id}} - User: - fields: - email: - type: - name: String - required: true - id: - type: - name: Int - required: true - name: - type: - name: String - required: true - phone: - type: - name: String - username: - type: - name: String - required: true - website: - type: - name: String diff --git a/examples/jsonplaceholder_batch.json b/examples/jsonplaceholder_batch.json deleted file mode 100644 index 72059725b7..0000000000 --- a/examples/jsonplaceholder_batch.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "server": { - "port": 8000 - }, - "upstream": { - "httpCache": 42, - "batch": { - "maxSize": 1000, - "delay": 1, - "headers": [] - } - }, - "schema": { - "query": "Query" - }, - "types": { - "Post": { - "fields": { - "body": { - "type": { - "name": "String", - "required": true - } - }, - "id": { - "type": { - "name": "Int", - "required": true - } - }, - "title": { - "type": { - "name": "String", - "required": true - } - }, - "user": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users", - "query": [ - { - "key": "id", - "value": "{{value.userId}}" - } - ], - "batchKey": ["id"] - } - }, - "userId": { - "type": { - "name": "Int", - "required": true - } - } - } - }, - "Query": { - "fields": { - "posts": { - "type": { - "list": { - "name": "Post" - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/posts" - } - } - } - }, - "User": { - "fields": { - "email": { - "type": { - "name": "String", - "required": true - } - }, - "id": { - "type": { - "name": "Int", - "required": true - } - }, - "name": { - "type": { - "name": "String", - "required": true - } - }, - "phone": { - "type": { - "name": "String" - } - }, - "username": { - "type": { - "name": "String", - "required": true - } - }, - "website": { - "type": { - "name": "String" - } - } - } - } - } -} diff --git a/examples/jsonplaceholder_batch.yml b/examples/jsonplaceholder_batch.yml deleted file mode 100644 index 78ac2980f9..0000000000 --- a/examples/jsonplaceholder_batch.yml +++ /dev/null @@ -1,71 +0,0 @@ -server: - port: 8000 -upstream: - httpCache: 42 - batch: - maxSize: 1000 - delay: 1 - headers: [] -schema: - query: Query -types: - Post: - fields: - body: - type: - name: String - required: true - id: - type: - name: Int - required: true - title: - type: - name: String - required: true - user: - type: - name: User - http: - url: http://jsonplaceholder.typicode.com/users - query: - - key: id - value: "{{value.userId}}" - batchKey: - - id - userId: - type: - name: Int - required: true - Query: - fields: - posts: - type: - list: - name: Post - http: - url: http://jsonplaceholder.typicode.com/posts - User: - fields: - email: - type: - name: String - required: true - id: - type: - name: Int - required: true - name: - type: - name: String - required: true - phone: - type: - name: String - username: - type: - name: String - required: true - website: - type: - name: String diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index ffb4d75c7e..e76f874e55 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -2,32 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", - "required": [ - "schema" - ], "properties": { - "enums": { - "description": "A map of all the enum types in the schema", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Enum" - } - }, - "links": { - "description": "A list of all links in the schema.", - "type": "array", - "items": { - "$ref": "#/definitions/Link" - } - }, - "schema": { - "description": "Specifies the entry points for query and mutation in the generated GraphQL schema.", - "allOf": [ - { - "$ref": "#/definitions/RootSchema" - } - ] - }, "server": { "description": "Dictates how the server behaves and helps tune tailcall for all ingress requests. Features such as request batching, SSL, HTTP2 etc. can be configured here.", "default": {}, @@ -45,21 +20,6 @@ } ] }, - "types": { - "description": "A map of all the types in the schema.", - "default": {}, - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Type" - } - }, - "unions": { - "description": "A map of all the union types in the schema.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Union" - } - }, "upstream": { "description": "Dictates how tailcall should handle upstream requests/responses. Tuning upstream can improve performance and reliability for connections.", "default": {}, @@ -71,44 +31,6 @@ } }, "definitions": { - "AddField": { - "description": "The @addField operator simplifies data structures and queries by adding a field that inlines or flattens a nested field or node within your schema. more info [here](https://tailcall.run/docs/guides/operators/#addfield)", - "type": "object", - "required": [ - "name", - "path" - ], - "properties": { - "name": { - "description": "Name of the new field to be added", - "type": "string" - }, - "path": { - "description": "Path of the data where the field should point to", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "Alias": { - "description": "The @alias directive indicates that aliases of one enum value.", - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - } - } - }, "Apollo": { "type": "object", "required": [ @@ -147,34 +69,6 @@ } } }, - "Arg": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "default_value": true, - "doc": { - "type": [ - "string", - "null" - ] - }, - "modify": { - "anyOf": [ - { - "$ref": "#/definitions/Modify" - }, - { - "type": "null" - } - ] - }, - "type": { - "$ref": "#/definitions/Type2" - } - } - }, "Batch": { "type": "object", "properties": { @@ -206,45 +100,6 @@ "title": "Bytes", "description": "Field whose value is a sequence of bytes." }, - "Cache": { - "description": "The @cache operator enables caching for the query, field or type it is applied to.", - "type": "object", - "required": [ - "maxAge" - ], - "properties": { - "maxAge": { - "description": "Specifies the duration, in milliseconds, of how long the value has to be stored in the cache.", - "type": "integer", - "format": "uint64", - "minimum": 1.0 - } - }, - "additionalProperties": false - }, - "Call": { - "description": "Provides the ability to refer to multiple fields in the Query or Mutation root.", - "type": "object", - "required": [ - "steps" - ], - "properties": { - "dedupe": { - "description": "Enables deduplication of IO operations to enhance performance.\n\nThis flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: May lead to issues with APIs that expect unique results for identical inputs, such as nonce-based APIs.", - "type": [ - "boolean", - "null" - ] - }, - "steps": { - "description": "Steps are composed together to form a call. If you have multiple steps, the output of the previous step is passed as input to the next step.", - "type": "array", - "items": { - "$ref": "#/definitions/Step" - } - } - } - }, "Cors": { "description": "Type to configure Cross-Origin Resource Sharing (CORS) for a server.", "type": "object", @@ -322,34 +177,6 @@ "title": "DateTime", "description": "Field whose value conforms to the standard datetime format as specified in RFC 3339 (https://datatracker.ietf.org/doc/html/rfc3339\")." }, - "Directive": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "arguments": { - "type": "object", - "additionalProperties": true - }, - "name": { - "type": "string" - } - } - }, - "Discriminate": { - "description": "The `@discriminate` directive is used to drive Tailcall discriminator to use a field of an object to resolve the type. For example with the directive applied on a field `@discriminate(field: \"object_type\")` and the given value `{\"foo\": \"bar\", \"object_type\": \"Buzz\"}` the resolved type of the object will be `Buzz`. If `field` is not applied it defaults to \"type\". The `field` does not have to be part of the GraphQL Schema, but it is required to be part of the JSON response. In case this field is missing from the response an appropriate error message will be displayed.", - "type": "object", - "properties": { - "field": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, "Email": { "title": "Email", "description": "Field whose value conforms to the standard internet email address format as specified in HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address." @@ -358,243 +185,6 @@ "title": "Empty", "description": "Empty scalar type represents an empty value." }, - "Encoding": { - "type": "string", - "enum": [ - "ApplicationJson", - "ApplicationXWwwFormUrlencoded" - ] - }, - "Enum": { - "description": "Definition of GraphQL enum type", - "type": "object", - "required": [ - "variants" - ], - "properties": { - "doc": { - "type": [ - "string", - "null" - ] - }, - "variants": { - "type": "array", - "items": { - "$ref": "#/definitions/Variant" - }, - "uniqueItems": true - } - } - }, - "Expr": { - "description": "The `@expr` operators allows you to specify an expression that can evaluate to a value. The expression can be a static value or built form a Mustache template. schema.", - "type": "object", - "required": [ - "body" - ], - "properties": { - "body": true - }, - "additionalProperties": false - }, - "Field": { - "description": "A field definition containing all the metadata information about resolving a field.", - "type": [ - "object", - "array" - ], - "items": { - "$ref": "#/definitions/Resolver" - }, - "properties": { - "args": { - "description": "Map of argument name and its definition.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Arg" - } - }, - "cache": { - "description": "Sets the cache configuration for a field", - "anyOf": [ - { - "$ref": "#/definitions/Cache" - }, - { - "type": "null" - } - ] - }, - "default_value": { - "description": "Stores the default value for the field" - }, - "directives": { - "description": "Any additional directives", - "type": "array", - "items": { - "$ref": "#/definitions/Directive" - } - }, - "discriminate": { - "description": "Used to overwrite the default discrimination strategy", - "anyOf": [ - { - "$ref": "#/definitions/Discriminate" - }, - { - "type": "null" - } - ] - }, - "doc": { - "description": "Publicly visible documentation for the field.", - "type": [ - "string", - "null" - ] - }, - "modify": { - "description": "Allows modifying existing fields.", - "anyOf": [ - { - "$ref": "#/definitions/Modify" - }, - { - "type": "null" - } - ] - }, - "omit": { - "description": "Omits a field from public consumption.", - "anyOf": [ - { - "$ref": "#/definitions/Omit" - }, - { - "type": "null" - } - ] - }, - "protected": { - "description": "Marks field as protected by auth provider", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Protected" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": "Refers to the type of the value the field can be resolved to.", - "allOf": [ - { - "$ref": "#/definitions/Type2" - } - ] - } - } - }, - "GraphQL": { - "description": "The @graphQL operator allows to specify GraphQL API server request to fetch data from.", - "type": "object", - "required": [ - "name", - "url" - ], - "properties": { - "args": { - "description": "Named arguments for the requested field. More info [here](https://tailcall.run/docs/guides/operators/#args)", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/KeyValue" - } - }, - "batch": { - "description": "If the upstream GraphQL server supports request batching, you can specify the 'batch' argument to batch several requests into a single batch request.\n\nMake sure you have also specified batch settings to the `@upstream` and to the `@graphQL` operator.", - "type": "boolean" - }, - "dedupe": { - "description": "Enables deduplication of IO operations to enhance performance.\n\nThis flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: May lead to issues with APIs that expect unique results for identical inputs, such as nonce-based APIs.", - "type": "boolean" - }, - "headers": { - "description": "The headers parameter allows you to customize the headers of the GraphQL request made by the `@graphQL` operator. It is used by specifying a key-value map of header names and their values.", - "type": "array", - "items": { - "$ref": "#/definitions/KeyValue" - } - }, - "name": { - "description": "Specifies the root field on the upstream to request data from. This maps a field in your schema to a field in the upstream schema. When a query is received for this field, Tailcall requests data from the corresponding upstream field.", - "type": "string" - }, - "url": { - "description": "This refers URL of the API.", - "type": "string" - } - }, - "additionalProperties": false - }, - "Grpc": { - "description": "The @grpc operator indicates that a field or node is backed by a gRPC API.\n\nFor instance, if you add the @grpc operator to the `users` field of the Query type with a service argument of `NewsService` and method argument of `GetAllNews`, it signifies that the `users` field is backed by a gRPC API. The `service` argument specifies the name of the gRPC service. The `method` argument specifies the name of the gRPC method. In this scenario, the GraphQL server will make a gRPC request to the gRPC endpoint specified when the `users` field is queried.", - "type": "object", - "required": [ - "method", - "url" - ], - "properties": { - "batchKey": { - "description": "The `batchKey` dictates the path Tailcall will follow to group the returned items from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).", - "type": "array", - "items": { - "type": "string" - } - }, - "body": { - "description": "This refers to the arguments of your gRPC call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added in the body in `protobuf` format." - }, - "dedupe": { - "description": "Enables deduplication of IO operations to enhance performance.\n\nThis flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: May lead to issues with APIs that expect unique results for identical inputs, such as nonce-based APIs.", - "type": [ - "boolean", - "null" - ] - }, - "headers": { - "description": "The `headers` parameter allows you to customize the headers of the HTTP request made by the `@grpc` operator. It is used by specifying a key-value map of header names and their values. Note: content-type is automatically set to application/grpc", - "type": "array", - "items": { - "$ref": "#/definitions/KeyValue" - } - }, - "method": { - "description": "This refers to the gRPC method you're going to call. For instance `GetAllNews`.", - "type": "string" - }, - "onResponseBody": { - "description": "Specifies a JavaScript function to be executed after receiving the response body. This function can modify or transform the response body before it's sent back to the client.", - "type": [ - "string", - "null" - ] - }, - "select": { - "description": "You can use `select` with mustache syntax to re-construct the directives response to the desired format. This is useful when data are deeply nested or want to keep specific fields only from the response.\n\n* EXAMPLE 1: if we have a call that returns `{ \"user\": { \"items\": [...], ... } ... }` we can use `\"{{.user.items}}\"`, to extract the `items`. * EXAMPLE 2: if we have a call that returns `{ \"foo\": \"bar\", \"fizz\": { \"buzz\": \"eggs\", ... }, ... }` we can use { foo: \"{{.foo}}\", buzz: \"{{.fizz.buzz}}\" }`" - }, - "url": { - "description": "This refers to URL of the API.", - "type": "string" - } - }, - "additionalProperties": false - }, "Headers": { "type": "object", "properties": { @@ -643,110 +233,6 @@ } } }, - "Http": { - "description": "The @http operator indicates that a field or node is backed by a REST API.\n\nFor instance, if you add the @http operator to the `users` field of the Query type with a path argument of `\"/users\"`, it signifies that the `users` field is backed by a REST API. The path argument specifies the path of the REST API. In this scenario, the GraphQL server will make a GET request to the API endpoint specified when the `users` field is queried.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "batchKey": { - "description": "The `batchKey` dictates the path Tailcall will follow to group the returned items from the batch request. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).", - "type": "array", - "items": { - "type": "string" - } - }, - "body": { - "description": "The body of the API call. It's used for methods like POST or PUT that send data to the server. You can pass it as a static object or use a Mustache template to substitute variables from the GraphQL variables.", - "type": [ - "string", - "null" - ] - }, - "dedupe": { - "description": "Enables deduplication of IO operations to enhance performance.\n\nThis flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: May lead to issues with APIs that expect unique results for identical inputs, such as nonce-based APIs.", - "type": [ - "boolean", - "null" - ] - }, - "encoding": { - "description": "The `encoding` parameter specifies the encoding of the request body. It can be `ApplicationJson` or `ApplicationXWwwFormUrlEncoded`. @default `ApplicationJson`.", - "allOf": [ - { - "$ref": "#/definitions/Encoding" - } - ] - }, - "headers": { - "description": "The `headers` parameter allows you to customize the headers of the HTTP request made by the `@http` operator. It is used by specifying a key-value map of header names and their values.", - "type": "array", - "items": { - "$ref": "#/definitions/KeyValue" - } - }, - "input": { - "description": "Schema of the input of the API call. It is automatically inferred in most cases.", - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { - "type": "null" - } - ] - }, - "method": { - "description": "This refers to the HTTP method of the API call. Commonly used methods include `GET`, `POST`, `PUT`, `DELETE` etc. @default `GET`.", - "allOf": [ - { - "$ref": "#/definitions/Method" - } - ] - }, - "onRequest": { - "description": "onRequest field in @http directive gives the ability to specify the request interception handler.", - "type": [ - "string", - "null" - ] - }, - "onResponseBody": { - "description": "Specifies a JavaScript function to be executed after receiving the response body. This function can modify or transform the response body before it's sent back to the client.", - "type": [ - "string", - "null" - ] - }, - "output": { - "description": "Schema of the output of the API call. It is automatically inferred in most cases.", - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { - "type": "null" - } - ] - }, - "query": { - "description": "This represents the query parameters of your API call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added to the URL. NOTE: Query parameter order is critical for batching in Tailcall. The first parameter referencing a field in the current value using mustache syntax is automatically selected as the batching parameter.", - "type": "array", - "items": { - "$ref": "#/definitions/URLQuery" - } - }, - "select": { - "description": "You can use `select` with mustache syntax to re-construct the directives response to the desired format. This is useful when data are deeply nested or want to keep specific fields only from the response.\n\n* EXAMPLE 1: if we have a call that returns `{ \"user\": { \"items\": [...], ... } ... }` we can use `\"{{.user.items}}\"`, to extract the `items`. * EXAMPLE 2: if we have a call that returns `{ \"foo\": \"bar\", \"fizz\": { \"buzz\": \"eggs\", ... }, ... }` we can use { foo: \"{{.foo}}\", buzz: \"{{.fizz.buzz}}\" }`" - }, - "url": { - "description": "This refers to URL of the API.", - "type": "string" - } - }, - "additionalProperties": false - }, "HttpVersion": { "type": "string", "enum": [ @@ -774,17 +260,6 @@ "title": "Int8", "description": "Field whose value is an 8-bit signed integer." }, - "JS": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, "JSON": { "title": "JSON", "description": "Field whose value conforms to the standard JSON format as specified in RFC 8259 (https://datatracker.ietf.org/doc/html/rfc8259)." @@ -804,112 +279,6 @@ } } }, - "Link": { - "description": "The @link directive allows you to import external resources, such as configuration – which will be merged into the config importing it –, or a .proto file – which will be later used by `@grpc` directive –.", - "type": "object", - "properties": { - "headers": { - "description": "Custom headers for gRPC reflection server.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/KeyValue" - } - }, - "id": { - "description": "The id of the link. It is used to reference the link in the schema.", - "type": [ - "string", - "null" - ] - }, - "meta": { - "description": "Additional metadata pertaining to the linked resource." - }, - "src": { - "description": "The source of the link. It can be a URL or a path to a file. If a path is provided, it is relative to the file that imports the link.", - "type": "string" - }, - "type": { - "description": "The type of the link. It can be `Config`, or `Protobuf`.", - "allOf": [ - { - "$ref": "#/definitions/LinkType" - } - ] - } - }, - "additionalProperties": false - }, - "LinkType": { - "oneOf": [ - { - "description": "Points to another Tailcall Configuration file. The imported configuration will be merged into the importing configuration.", - "type": "string", - "enum": [ - "Config" - ] - }, - { - "description": "Points to a Protobuf file. The imported Protobuf file will be used by the `@grpc` directive. If your API exposes a reflection endpoint, you should set the type to `Grpc` instead.", - "type": "string", - "enum": [ - "Protobuf" - ] - }, - { - "description": "Points to a JS file. The imported JS file will be used by the `@js` directive.", - "type": "string", - "enum": [ - "Script" - ] - }, - { - "description": "Points to a Cert file. The imported Cert file will be used by the server to serve over HTTPS.", - "type": "string", - "enum": [ - "Cert" - ] - }, - { - "description": "Points to a Key file. The imported Key file will be used by the server to serve over HTTPS.", - "type": "string", - "enum": [ - "Key" - ] - }, - { - "description": "A trusted document that contains GraphQL operations (queries, mutations) that can be exposed a REST API using the `@rest` directive.", - "type": "string", - "enum": [ - "Operation" - ] - }, - { - "description": "Points to a Htpasswd file. The imported Htpasswd file will be used by the server to authenticate users.", - "type": "string", - "enum": [ - "Htpasswd" - ] - }, - { - "description": "Points to a Jwks file. The imported Jwks file will be used by the server to authenticate users.", - "type": "string", - "enum": [ - "Jwks" - ] - }, - { - "description": "Points to a reflection endpoint. The imported reflection endpoint will be used by the `@grpc` directive to resolve data from gRPC services.", - "type": "string", - "enum": [ - "Grpc" - ] - } - ] - }, "Method": { "type": "string", "enum": [ @@ -924,29 +293,6 @@ "TRACE" ] }, - "Modify": { - "type": "object", - "properties": { - "name": { - "type": [ - "string", - "null" - ] - }, - "omit": { - "type": [ - "boolean", - "null" - ] - } - }, - "additionalProperties": false - }, - "Omit": { - "description": "Used to omit a field from public consumption.", - "type": "object", - "additionalProperties": false - }, "OtlpExporter": { "description": "Output the opentelemetry data to otlp collector", "type": "object", @@ -984,27 +330,11 @@ }, "PrometheusFormat": { "description": "Output format for prometheus data", - "type": "string", - "enum": [ - "text", - "protobuf" - ] - }, - "Protected": { - "description": "Specifies the authentication requirements for accessing a field or type.\n\nThis allows you to control access by listing the IDs of authentication providers. - If `id` is not provided, all available providers must authorize the request. - If multiple provider IDs are listed, the request must be authorized by all of them.\n\nExample: If you want only specific providers to allow access, include their IDs in the list. Otherwise, leave it empty to require authorization from all available providers.", - "type": "object", - "properties": { - "id": { - "description": "List of authentication provider IDs that can access this field or type. - Leave empty to require authorization from all providers. - Include multiple IDs to require authorization from each one.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - } - } + "type": "string", + "enum": [ + "text", + "protobuf" + ] }, "Proxy": { "type": "object", @@ -1017,105 +347,6 @@ } } }, - "Resolver": { - "oneOf": [ - { - "type": "object", - "required": [ - "http" - ], - "properties": { - "http": { - "$ref": "#/definitions/Http" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "grpc" - ], - "properties": { - "grpc": { - "$ref": "#/definitions/Grpc" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "graphql" - ], - "properties": { - "graphql": { - "$ref": "#/definitions/GraphQL" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "call" - ], - "properties": { - "call": { - "$ref": "#/definitions/Call" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "js" - ], - "properties": { - "js": { - "$ref": "#/definitions/JS" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "expr" - ], - "properties": { - "expr": { - "$ref": "#/definitions/Expr" - } - }, - "additionalProperties": false - } - ] - }, - "RootSchema": { - "type": "object", - "properties": { - "mutation": { - "type": [ - "string", - "null" - ] - }, - "query": { - "type": [ - "string", - "null" - ] - }, - "subscription": { - "type": [ - "string", - "null" - ] - } - } - }, "Routes": { "type": "object", "properties": { @@ -1305,31 +536,6 @@ } } }, - "Step": { - "description": "Provides the ability to refer to a field defined in the root Query or Mutation.", - "type": "object", - "properties": { - "args": { - "description": "The arguments that will override the actual arguments of the field.", - "type": "object", - "additionalProperties": true - }, - "mutation": { - "description": "The name of the field on the `Mutation` type that you want to call.", - "type": [ - "string", - "null" - ] - }, - "query": { - "description": "The name of the field on the `Query` type that you want to call.", - "type": [ - "string", - "null" - ] - } - } - }, "Telemetry": { "description": "The @telemetry directive facilitates seamless integration with OpenTelemetry, enhancing the observability of your GraphQL services powered by Tailcall. By leveraging this directive, developers gain access to valuable insights into the performance and behavior of their applications.", "type": "object", @@ -1406,121 +612,6 @@ } ] }, - "Type": { - "description": "Represents a GraphQL type. A type can be an object, interface, enum or scalar.", - "type": [ - "object", - "array" - ], - "items": { - "$ref": "#/definitions/Resolver" - }, - "required": [ - "fields" - ], - "properties": { - "added_fields": { - "description": "Additional fields to be added to the type", - "type": "array", - "items": { - "$ref": "#/definitions/AddField" - } - }, - "cache": { - "description": "Setting to indicate if the type can be cached.", - "anyOf": [ - { - "$ref": "#/definitions/Cache" - }, - { - "type": "null" - } - ] - }, - "directives": { - "description": "Any additional directives", - "type": "array", - "items": { - "$ref": "#/definitions/Directive" - } - }, - "doc": { - "description": "Documentation for the type that is publicly visible.", - "type": [ - "string", - "null" - ] - }, - "fields": { - "description": "A map of field name and its definition.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Field" - } - }, - "implements": { - "description": "Interfaces that the type implements.", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "protected": { - "description": "Marks field as protected by auth providers", - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/Protected" - }, - { - "type": "null" - } - ] - } - } - }, - "Type2": { - "description": "Type to represent GraphQL type usage with modifiers [spec](https://spec.graphql.org/October2021/#sec-Wrapping-Types)", - "anyOf": [ - { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "description": "Name of the type", - "type": "string" - }, - "required": { - "description": "Flag to indicate the type is required.", - "type": "boolean" - } - } - }, - { - "type": "object", - "required": [ - "list" - ], - "properties": { - "list": { - "description": "Type is a list", - "allOf": [ - { - "$ref": "#/definitions/Type2" - } - ] - }, - "required": { - "description": "Flag to indicate the type is required.", - "type": "boolean" - } - } - } - ] - }, "UInt128": { "title": "UInt128", "description": "Field whose value is a 128-bit unsigned integer." @@ -1541,52 +632,6 @@ "title": "UInt8", "description": "Field whose value is an 8-bit unsigned integer." }, - "URLQuery": { - "description": "The URLQuery input type represents a query parameter to be included in a URL.", - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "description": "The key or name of the query parameter.", - "type": "string" - }, - "skipEmpty": { - "description": "Determines whether to ignore query parameters with empty values.", - "type": [ - "boolean", - "null" - ] - }, - "value": { - "description": "The actual value or a mustache template to resolve the value dynamically for the query parameter.", - "type": "string" - } - } - }, - "Union": { - "type": "object", - "required": [ - "types" - ], - "properties": { - "doc": { - "type": [ - "string", - "null" - ] - }, - "types": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - } - } - }, "Upstream": { "description": "The `upstream` directive allows you to control various aspects of the upstream server connection. This includes settings like connection timeouts, keep-alive intervals, and more. If not specified, default values are used.", "type": "object", @@ -1737,97 +782,6 @@ "Url": { "title": "Url", "description": "Field whose value conforms to the standard URL format as specified in RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986)." - }, - "Variant": { - "description": "Definition of GraphQL value", - "type": "object", - "required": [ - "name" - ], - "properties": { - "alias": { - "anyOf": [ - { - "$ref": "#/definitions/Alias" - }, - { - "type": "null" - } - ] - }, - "name": { - "type": "string" - } - } - }, - "schema": { - "oneOf": [ - { - "type": "string", - "enum": [ - "Str", - "Num", - "Bool", - "Empty", - "Any" - ] - }, - { - "type": "object", - "required": [ - "Obj" - ], - "properties": { - "Obj": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - } - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "Arr" - ], - "properties": { - "Arr": { - "$ref": "#/definitions/schema" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "Opt" - ], - "properties": { - "Opt": { - "$ref": "#/definitions/schema" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "Enum" - ], - "properties": { - "Enum": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - } - }, - "additionalProperties": false - } - ] } } } \ No newline at end of file diff --git a/src/core/config/config.rs b/src/core/config/config.rs index cccfa0eeaf..a58715bec1 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -1,7 +1,7 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::fmt::{self, Display}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use async_graphql::parser::types::ServiceDocument; use derive_setters::Setters; use indexmap::IndexMap; @@ -56,27 +56,28 @@ pub struct Config { /// /// Specifies the entry points for query and mutation in the generated /// GraphQL schema. + #[serde(skip)] pub schema: RootSchema, /// /// A map of all the types in the schema. - #[serde(default)] + #[serde(skip)] #[setters(skip)] pub types: BTreeMap, /// /// A map of all the union types in the schema. - #[serde(default, skip_serializing_if = "is_default")] + #[serde(skip)] pub unions: BTreeMap, /// /// A map of all the enum types in the schema - #[serde(default, skip_serializing_if = "is_default")] + #[serde(skip)] pub enums: BTreeMap, /// /// A list of all links in the schema. - #[serde(default, skip_serializing_if = "is_default")] + #[serde(skip)] pub links: Vec, /// Enable [opentelemetry](https://opentelemetry.io) support @@ -87,40 +88,31 @@ pub struct Config { /// /// Represents a GraphQL type. /// A type can be an object, interface, enum or scalar. -#[derive( - Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema, MergeRight, -)] +#[derive(Clone, Debug, Default, PartialEq, Eq, MergeRight)] pub struct Type { /// /// A map of field name and its definition. pub fields: BTreeMap, - #[serde(default, skip_serializing_if = "is_default")] /// /// Additional fields to be added to the type pub added_fields: Vec, - #[serde(default, skip_serializing_if = "is_default")] /// /// Documentation for the type that is publicly visible. pub doc: Option, - #[serde(default, skip_serializing_if = "is_default")] /// /// Interfaces that the type implements. pub implements: BTreeSet, - #[serde(default, skip_serializing_if = "is_default")] /// /// Setting to indicate if the type can be cached. pub cache: Option, /// /// Marks field as protected by auth providers - #[serde(default)] pub protected: Option, /// /// Apollo federation entity resolver. - #[serde(flatten, default, skip_serializing_if = "is_default")] pub resolvers: ResolverSet, /// /// Any additional directives - #[serde(default, skip_serializing_if = "is_default")] pub directives: Vec, } @@ -158,59 +150,38 @@ impl Type { } } -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - Default, - Setters, - PartialEq, - Eq, - schemars::JsonSchema, - MergeRight, -)] +#[derive(Clone, Debug, Default, Setters, PartialEq, Eq, MergeRight)] #[setters(strip_option)] pub struct RootSchema { pub query: Option, - #[serde(default, skip_serializing_if = "is_default")] pub mutation: Option, - #[serde(default, skip_serializing_if = "is_default")] pub subscription: Option, } /// /// A field definition containing all the metadata information about resolving a /// field. -#[derive( - Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq, Eq, schemars::JsonSchema, -)] +#[derive(Clone, Debug, Default, Setters, PartialEq, Eq)] #[setters(strip_option)] pub struct Field { /// /// Refers to the type of the value the field can be resolved to. - #[serde(rename = "type", default, skip_serializing_if = "is_default")] pub type_of: crate::core::Type, /// /// Map of argument name and its definition. - #[serde(default, skip_serializing_if = "is_default")] - #[schemars(with = "HashMap::")] pub args: IndexMap, /// /// Publicly visible documentation for the field. - #[serde(default, skip_serializing_if = "is_default")] pub doc: Option, /// /// Allows modifying existing fields. - #[serde(default, skip_serializing_if = "is_default")] pub modify: Option, /// /// Omits a field from public consumption. - #[serde(default, skip_serializing_if = "is_default")] pub omit: Option, /// @@ -219,12 +190,10 @@ pub struct Field { /// /// Stores the default value for the field - #[serde(default, skip_serializing_if = "is_default")] pub default_value: Option, /// /// Marks field as protected by auth provider - #[serde(default)] pub protected: Option, /// @@ -233,12 +202,10 @@ pub struct Field { /// /// Resolver for the field - #[serde(flatten, default, skip_serializing_if = "is_default")] pub resolvers: ResolverSet, /// /// Any additional directives - #[serde(default, skip_serializing_if = "is_default")] pub directives: Vec, } @@ -288,32 +255,26 @@ impl Field { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Inline { pub path: Vec, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] +#[derive(Default, Clone, Debug, PartialEq, Eq)] pub struct Arg { - #[serde(rename = "type")] pub type_of: crate::core::Type, - #[serde(default, skip_serializing_if = "is_default")] pub doc: Option, - #[serde(default, skip_serializing_if = "is_default")] pub modify: Option, - #[serde(default, skip_serializing_if = "is_default")] pub default_value: Option, } -#[derive( - Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema, MergeRight, -)] +#[derive(Clone, Debug, Default, PartialEq, Eq, MergeRight)] pub struct Union { pub types: BTreeSet, pub doc: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, MergeRight)] +#[derive(Clone, Debug, PartialEq, Eq, MergeRight)] /// Definition of GraphQL enum type pub struct Enum { pub variants: BTreeSet, @@ -321,26 +282,14 @@ pub struct Enum { } /// Definition of GraphQL value -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - schemars::JsonSchema, - MergeRight, -)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, MergeRight)] pub struct Variant { pub name: String, // directive: alias pub alias: Option, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub enum GraphQLOperationType { #[default] Query, @@ -442,8 +391,7 @@ impl Config { pub fn from_source(source: Source, schema: &str) -> Result { match source { Source::GraphQL => Ok(Config::from_sdl(schema).to_result()?), - Source::Json => Ok(Config::from_json(schema)?), - Source::Yml => Ok(Config::from_yaml(schema)?), + _ => Err(anyhow!("Only the graphql config is currently supported")), } } diff --git a/src/core/config/reader.rs b/src/core/config/reader.rs index f6d539360f..175291e865 100644 --- a/src/core/config/reader.rs +++ b/src/core/config/reader.rs @@ -273,40 +273,33 @@ mod reader_tests { async fn test_all() { let runtime = crate::core::runtime::test::init(None); + let server = start_mock_server(); let mut cfg = Config::default(); - cfg.schema.query = Some("Test".to_string()); - cfg = cfg.types([("Test", Type::default())].to_vec()); + cfg = cfg.types([("User", Type::default())].to_vec()); - let server = start_mock_server(); - let header_server = server.mock(|when, then| { - when.method(httpmock::Method::GET).path("/bar.graphql"); + let foo_mock = server.mock(|when, then| { + when.method(httpmock::Method::GET).path("/foo.graphql"); then.status(200).body(cfg.to_sdl()); }); - let json = runtime - .file - .read("examples/jsonplaceholder.json") - .await - .unwrap(); + let mut cfg = Config::default(); + cfg.schema.query = Some("Test".to_string()); + cfg = cfg.types([("Test", Type::default())].to_vec()); - let foo_json_server = server.mock(|when, then| { - when.method(httpmock::Method::GET).path("/foo.json"); - then.status(200).body(json); + let bar_mock = server.mock(|when, then| { + when.method(httpmock::Method::GET).path("/bar.graphql"); + then.status(200).body(cfg.to_sdl()); }); let port = server.port(); - let files: Vec = [ - "examples/jsonplaceholder.yml", // config from local file - format!("http://localhost:{port}/bar.graphql").as_str(), // with content-type header - format!("http://localhost:{port}/foo.json").as_str(), // with url extension - ] - .iter() - .map(|x| x.to_string()) - .collect(); + let files = vec![ + format!("http://localhost:{port}/foo.graphql"), + format!("http://localhost:{port}/bar.graphql"), + ]; let cr = ConfigReader::init(runtime); let c = cr.read_all(&files).await.unwrap(); assert_eq!( - ["Post", "Query", "Test", "User"] + ["Test", "User"] .iter() .map(|i| i.to_string()) .collect::>(), @@ -315,22 +308,18 @@ mod reader_tests { .map(|i| i.to_string()) .collect::>() ); - foo_json_server.assert(); // checks if the request was actually made - header_server.assert(); + foo_mock.assert(); + bar_mock.assert(); } #[tokio::test] async fn test_local_files() { let runtime = crate::core::runtime::test::init(None); - let files: Vec = [ - "examples/jsonplaceholder.yml", - "examples/jsonplaceholder.graphql", - "examples/jsonplaceholder.json", - ] - .iter() - .map(|x| x.to_string()) - .collect(); + let files: Vec = ["examples/jsonplaceholder.graphql"] + .iter() + .map(|x| x.to_string()) + .collect(); let cr = ConfigReader::init(runtime); let c = cr.read_all(&files).await.unwrap(); assert_eq!( diff --git a/src/core/config/transformer/fixtures/nested-unions-recursive.graphql b/src/core/config/transformer/fixtures/nested-unions-recursive.graphql new file mode 100644 index 0000000000..2fb810f272 --- /dev/null +++ b/src/core/config/transformer/fixtures/nested-unions-recursive.graphql @@ -0,0 +1,32 @@ +schema { + query: Query +} + +type T1 { + t1: String +} + +type T2 { + t2: Int +} + +type T3 { + t3: Boolean + t33: Float! +} + +type T4 { + t4: String +} + +type T5 { + t5: Boolean +} + +union U1 = T1 | T2 | T3 | U2 +union U2 = T3 | T4 | U1 +union U = U1 | U2 | T5 + +type Query { + test(u: U!): U @http(url: "http://localhost/users/{{args.u}}") +} diff --git a/src/core/config/transformer/fixtures/nested-unions.graphql b/src/core/config/transformer/fixtures/nested-unions.graphql new file mode 100644 index 0000000000..dfb418246c --- /dev/null +++ b/src/core/config/transformer/fixtures/nested-unions.graphql @@ -0,0 +1,32 @@ +schema { + query: Query +} + +type T1 { + t1: String +} + +type T2 { + t2: Int +} + +type T3 { + t3: Boolean + t33: Float! +} + +type T4 { + t4: String +} + +type T5 { + t5: Boolean +} + +union U1 = T1 | T2 | T3 +union U2 = T3 | T4 +union U = U1 | U2 | T5 + +type Query { + test(u: U!): U @http(url: "http://localhost/users/{{args.u}}") +} diff --git a/src/core/config/transformer/fixtures/recursive-input.graphql b/src/core/config/transformer/fixtures/recursive-input.graphql new file mode 100644 index 0000000000..23899c7844 --- /dev/null +++ b/src/core/config/transformer/fixtures/recursive-input.graphql @@ -0,0 +1,17 @@ +schema @server(port: 8000) { + query: Query +} + +type Bar { + name: Foo + rec: Bar +} + +type Foo { + name: String +} + +type Query { + bars(filter: Bar): String + @graphQL(url: "http://localhost", args: [{key: "baz", value: "{{.args.baz}}"}], name: "bars") +} diff --git a/src/core/config/transformer/fixtures/union-in-type.graphql b/src/core/config/transformer/fixtures/union-in-type.graphql new file mode 100644 index 0000000000..8c58a1b0e1 --- /dev/null +++ b/src/core/config/transformer/fixtures/union-in-type.graphql @@ -0,0 +1,33 @@ +schema { + query: Query +} + +type T1 { + t1: String +} + +type T2 { + t2: Int +} + +type T3 { + t3: Boolean + t33: Float! +} + +type NU { + test: String + u: U +} + +type NNU { + other: Int + new: Boolean + nu: NU +} + +union U = T1 | T2 | T3 + +type Query { + test(nu: NU!, nnu: NNU): U @http(url: "http://localhost/users/{{args.nu.u}}") +} diff --git a/src/core/config/transformer/fixtures/union.graphql b/src/core/config/transformer/fixtures/union.graphql new file mode 100644 index 0000000000..71ab1f38cb --- /dev/null +++ b/src/core/config/transformer/fixtures/union.graphql @@ -0,0 +1,22 @@ +schema { + query: Query +} + +type T1 { + t1: String +} + +type T2 { + t2: Int +} + +type T3 { + t3: Boolean + t33: Float! +} + +union U = T1 | T2 | T3 + +type Query { + test(u: U!): U @http(url: "http://localhost/users/{{args.u}}") +} diff --git a/src/core/config/transformer/nested_unions.rs b/src/core/config/transformer/nested_unions.rs index faab9d98ed..f7dea98cd1 100644 --- a/src/core/config/transformer/nested_unions.rs +++ b/src/core/config/transformer/nested_unions.rs @@ -75,14 +75,12 @@ mod tests { use tailcall_valid::Validator; use super::NestedUnions; - use crate::core::config::Config; use crate::core::transform::Transform; + use crate::include_config; #[test] fn test_nested_unions() { - let config = - std::fs::read_to_string(tailcall_fixtures::configs::YAML_NESTED_UNIONS).unwrap(); - let config = Config::from_yaml(&config).unwrap(); + let config = include_config!("./fixtures/nested-unions.graphql").unwrap(); let config = NestedUnions.transform(config).to_result().unwrap(); assert_snapshot!(config.to_sdl()); @@ -90,10 +88,7 @@ mod tests { #[test] fn test_nested_unions_recursive() { - let config = - std::fs::read_to_string(tailcall_fixtures::configs::YAML_NESTED_UNIONS_RECURSIVE) - .unwrap(); - let config = Config::from_yaml(&config).unwrap(); + let config = include_config!("./fixtures/nested-unions-recursive.graphql").unwrap(); let error = NestedUnions.transform(config).to_result().unwrap_err(); assert_snapshot!(error); diff --git a/src/core/config/transformer/union_input_type.rs b/src/core/config/transformer/union_input_type.rs index 906650dd6d..ca44f83141 100644 --- a/src/core/config/transformer/union_input_type.rs +++ b/src/core/config/transformer/union_input_type.rs @@ -285,13 +285,12 @@ mod tests { use tailcall_valid::Validator; use super::UnionInputType; - use crate::core::config::Config; use crate::core::transform::Transform; + use crate::include_config; #[test] fn test_union() { - let config = std::fs::read_to_string(tailcall_fixtures::configs::YAML_UNION).unwrap(); - let config = Config::from_yaml(&config).unwrap(); + let config = include_config!("./fixtures/union.graphql").unwrap(); let config = UnionInputType.transform(config).to_result().unwrap(); assert_snapshot!(config.to_sdl()); @@ -299,9 +298,7 @@ mod tests { #[test] fn test_union_in_type() { - let config = - std::fs::read_to_string(tailcall_fixtures::configs::YAML_UNION_IN_TYPE).unwrap(); - let config = Config::from_yaml(&config).unwrap(); + let config = include_config!("./fixtures/union-in-type.graphql").unwrap(); let config = UnionInputType.transform(config).to_result().unwrap(); assert_snapshot!(config.to_sdl()); @@ -309,18 +306,14 @@ mod tests { #[test] fn test_nested_unions() { - let config = - std::fs::read_to_string(tailcall_fixtures::configs::YAML_NESTED_UNIONS).unwrap(); - let config = Config::from_yaml(&config).unwrap(); + let config = include_config!("./fixtures/nested-unions.graphql").unwrap(); let config = UnionInputType.transform(config).to_result().unwrap(); assert_snapshot!(config.to_sdl()); } #[test] fn test_recursive_input() { - let config = - std::fs::read_to_string(tailcall_fixtures::configs::YAML_RECURSIVE_INPUT).unwrap(); - let config = Config::from_yaml(&config).unwrap(); + let config = include_config!("./fixtures/recursive-input.graphql").unwrap(); let config = UnionInputType.transform(config).to_result().unwrap(); assert_snapshot!(config.to_sdl()); diff --git a/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml b/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml deleted file mode 100644 index 829afc48ff..0000000000 --- a/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml +++ /dev/null @@ -1,56 +0,0 @@ -schema: - query: Query - -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true - - T4: - fields: - t4: - type: - name: String - - T5: - fields: - t5: - type: - name: Boolean - - Query: - fields: - test: - type: - name: U - args: - u: - type: - name: U - required: true - http: - url: http://localhost/users/{{args.u}} - -unions: - U1: - types: ["T1", "T2", "T3", "U2"] - U2: - types: ["T3", "T4", "U1"] - U: - types: ["U1", "U2", "T5"] diff --git a/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml b/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml deleted file mode 100644 index 4672968a04..0000000000 --- a/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml +++ /dev/null @@ -1,56 +0,0 @@ -schema: - query: Query - -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true - - T4: - fields: - t4: - type: - name: String - - T5: - fields: - t5: - type: - name: Boolean - - Query: - fields: - test: - type: - name: U - args: - u: - type: - name: U - required: true - http: - url: http://localhost/users/{{args.u}} - -unions: - U1: - types: ["T1", "T2", "T3"] - U2: - types: ["T3", "T4"] - U: - types: ["U1", "U2", "T5"] diff --git a/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml b/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml deleted file mode 100644 index 43ee7c3c72..0000000000 --- a/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml +++ /dev/null @@ -1,34 +0,0 @@ -server: - port: 8000 -schema: - query: Query -types: - Bar: - fields: - name: - type: - name: Foo - rec: - type: - name: Bar - - Query: - fields: - bars: - type: - name: String - args: - filter: - type: - name: Bar - graphql: - args: - - key: baz - value: '{{.args.baz}}' - url: http://localhost - name: bars - Foo: - fields: - name: - type: - name: String diff --git a/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml b/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml deleted file mode 100644 index 66cf2edaae..0000000000 --- a/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml +++ /dev/null @@ -1,64 +0,0 @@ -schema: - query: Query - -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true - - NU: - fields: - test: - type: - name: String - u: - type: - name: U - - NNU: - fields: - other: - type: - name: Int - new: - type: - name: Boolean - nu: - type: - name: NU - - Query: - fields: - test: - type: - name: U - args: - nu: - type: - name: NU - required: true - nnu: - type: - name: NNU - http: - url: http://localhost/users/{{args.nu.u}} - -unions: - U: - types: ["T1", "T2", "T3"] diff --git a/tailcall-fixtures/fixtures/configs/yaml-union.yaml b/tailcall-fixtures/fixtures/configs/yaml-union.yaml deleted file mode 100644 index 1398fe227c..0000000000 --- a/tailcall-fixtures/fixtures/configs/yaml-union.yaml +++ /dev/null @@ -1,40 +0,0 @@ -schema: - query: Query - -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true - - Query: - fields: - test: - type: - name: U - args: - u: - type: - name: U - required: true - http: - url: http://localhost/users/{{args.u}} - -unions: - U: - types: ["T1", "T2", "T3"] diff --git a/tests/core/snapshots/test-enum-empty.md_error.snap b/tests/core/snapshots/test-enum-empty.md_error.snap index 1d843257e5..fa0959164e 100644 --- a/tests/core/snapshots/test-enum-empty.md_error.snap +++ b/tests/core/snapshots/test-enum-empty.md_error.snap @@ -1,10 +1,11 @@ --- source: tests/core/spec.rs expression: errors +snapshot_kind: text --- [ { - "message": "No variants found for enum", + "message": " --> 9:11\n |\n9 | enum Foo {}\n | ^---\n |\n = expected enum_value_definition", "trace": [], "description": null } diff --git a/tests/execution/batching-disabled.md b/tests/execution/batching-disabled.md index d95145be48..5783980497 100644 --- a/tests/execution/batching-disabled.md +++ b/tests/execution/batching-disabled.md @@ -1,66 +1,18 @@ # Batching disabled -```json @config -{ - "server": {}, - "upstream": { - "httpCache": 42, - "batch": { - "maxSize": 100, - "delay": 0, - "headers": [] - } - }, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "args": { - "id": { - "type": { - "name": "Int", - "required": true - } - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/{{.args.id}}" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - }, - "username": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server @upstream(httpCache: 42, batch: {maxSize: 100, delay: 0, headers: []}) { + query: Query +} + +type Query { + user(id: Int!): User @http(url: "http://jsonplaceholder.typicode.com/users/{{.args.id}}") +} + +type User { + id: Int + name: String + username: String } ``` diff --git a/tests/execution/batching.md b/tests/execution/batching.md index 648266d6bc..34eb5b6f0d 100644 --- a/tests/execution/batching.md +++ b/tests/execution/batching.md @@ -1,47 +1,17 @@ # Sending a batched graphql request -```json @config -{ - "server": { - "batchRequests": true - }, - "upstream": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server(batchRequests: true) @upstream { + query: Query +} + +type Query { + user: User @http(url: "http://jsonplaceholder.typicode.com/users/1") +} + +type User { + id: Int + name: String } ``` diff --git a/tests/execution/cache-control.md b/tests/execution/cache-control.md index 130452cd04..c359473e72 100644 --- a/tests/execution/cache-control.md +++ b/tests/execution/cache-control.md @@ -1,62 +1,18 @@ # Sending requests to verify Cache-Control behavior -```json @config -{ - "server": { - "headers": { - "cacheControl": true - } - }, - "upstream": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "args": { - "id": { - "type": { - "name": "Int" - } - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users", - "query": [ - { - "key": "id", - "value": "{{.args.id}}" - } - ] - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server(headers: {cacheControl: true}) @upstream { + query: Query +} + +type Query { + user(id: Int): User + @http(url: "http://jsonplaceholder.typicode.com/users", query: [{key: "id", value: "{{.args.id}}"}]) +} + +type User { + id: Int + name: String } ``` diff --git a/tests/execution/custom-headers.md b/tests/execution/custom-headers.md index 3a0c4f486d..9bf05ba6f4 100644 --- a/tests/execution/custom-headers.md +++ b/tests/execution/custom-headers.md @@ -1,41 +1,12 @@ # Custom Headers -```json @config -{ - "server": { - "headers": { - "custom": [ - { - "key": "x-id", - "value": "1" - }, - { - "key": "x-name", - "value": "John Doe" - } - ] - } - }, - "upstream": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "greet": { - "type": { - "name": "String" - }, - "expr": { - "body": "Hello World!" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server(headers: {custom: [{key: "x-id", value: "1"}, {key: "x-name", value: "John Doe"}]}) @upstream { + query: Query +} + +type Query { + greet: String @expr(body: "Hello World!") } ``` diff --git a/tests/execution/env-value.md b/tests/execution/env-value.md index 15a1ee7862..12eaa4db67 100644 --- a/tests/execution/env-value.md +++ b/tests/execution/env-value.md @@ -1,75 +1,21 @@ # Env value -```json @config -{ - "server": {}, - "schema": { - "query": "Query" - }, - "types": { - "Post": { - "fields": { - "body": { - "type": { - "name": "String" - }, - "cache": null - }, - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "title": { - "type": { - "name": "String" - }, - "cache": null - }, - "userId": { - "type": { - "name": "Int", - "required": true - }, - "cache": null - } - }, - "cache": null - }, - "Query": { - "fields": { - "post1": { - "type": { - "name": "Post" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/posts/{{.env.ID}}" - }, - "cache": null - }, - "post2": { - "type": { - "name": "Post" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/posts/{{.env.POST_ID}}" - }, - "cache": null - }, - "post3": { - "type": { - "name": "Post" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/posts/{{.env.NESTED_POST_ID}}" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server { + query: Query +} + +type Post { + body: String + id: Int + title: String + userId: Int! +} + +type Query { + post1: Post @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.ID}}") + post2: Post @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.POST_ID}}") + post3: Post @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.NESTED_POST_ID}}") } ``` diff --git a/tests/execution/https.md b/tests/execution/https.md deleted file mode 100644 index 93b418be13..0000000000 --- a/tests/execution/https.md +++ /dev/null @@ -1,61 +0,0 @@ -# Against a server with HTTPS - -```json @config -{ - "server": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "firstUser": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } -} -``` - -```yml @mock -- request: - method: GET - url: http://jsonplaceholder.typicode.com/users/1 - response: - status: 200 - body: - id: 1 - name: Leanne Graham -``` - -```yml @test -- method: POST - url: http://localhost:8080/graphql - body: - query: query { firstUser { name } } -``` diff --git a/tests/execution/recursive-types-json.md b/tests/execution/recursive-types-json.md deleted file mode 100644 index 3c3431047e..0000000000 --- a/tests/execution/recursive-types-json.md +++ /dev/null @@ -1,211 +0,0 @@ -# Recursive Type JSON - -```json @config -{ - "$schema": "./.tailcallrc.schema.json", - "upstream": { - "httpCache": 42 - }, - "schema": { - "query": "Query", - "mutation": "Mutation" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - } - } - } - }, - "Mutation": { - "fields": { - "createUser": { - "args": { - "user": { - "type": { - "name": "User" - } - } - }, - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/user", - "method": "POST", - "body": "{{.args.user}}" - } - } - } - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int", - "required": true - } - }, - "name": { - "type": { - "name": "String", - "required": true - } - }, - "connections": { - "type": { - "list": { - "name": "Connection" - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/connections/{{.value.id}}" - } - } - } - }, - "Connection": { - "fields": { - "type": { - "type": { - "name": "String" - } - }, - "user": { - "type": { - "name": "User" - } - } - } - } - } -} -``` - -```yml @mock -- request: - method: GET - url: http://jsonplaceholder.typicode.com/users/1 - response: - status: 200 - body: - id: 1 - name: User1 -- request: - method: GET - url: http://jsonplaceholder.typicode.com/connections/1 - response: - status: 200 - body: - - type: friend - user: - id: 2 - name: User2 - -- request: - method: GET - url: http://jsonplaceholder.typicode.com/connections/2 - response: - status: 200 - body: - - type: friend - user: - id: 3 - name: User3 - - type: coworker - user: - id: 4 - name: User4 - -- request: - method: POST - url: http://jsonplaceholder.typicode.com/user - body: - id: 111 - name: NewUser - connections: - - type: friend - user: - id: 1 - name: User1 - response: - status: 200 - body: - id: 111 - name: NewUser - -- request: - method: GET - url: http://jsonplaceholder.typicode.com/connections/111 - response: - status: 200 - body: - - type: friend - user: - id: 1 - name: User1 -``` - -```yml @test -- method: POST - url: http://localhost:8080/graphql - body: - query: | - query { - user { - name - id - connections { - type - user { - name - id - connections { - user { - name - id - } - } - } - } - } - } - -- method: POST - url: http://localhost:8080/graphql - body: - query: | - mutation { - createUser( - user: { - id: 111, - name: "NewUser", - connections: [ - { - type: "friend" - user: { - id: 1 - name: "User1" - } - } - ] - } - ) { - name - id - connections { - type - user { - name - id - } - } - } - } -``` diff --git a/tests/execution/ref-other-nested.md b/tests/execution/ref-other-nested.md index 9687d9177b..a5d87c839a 100644 --- a/tests/execution/ref-other-nested.md +++ b/tests/execution/ref-other-nested.md @@ -1,69 +1,25 @@ # Ref other nested -```json @config -{ - "server": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "firstUser": { - "type": { - "name": "User1" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - }, - "User1": { - "fields": { - "user1": { - "type": { - "name": "User2" - }, - "cache": null - } - }, - "cache": null - }, - "User2": { - "fields": { - "user2": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server { + query: Query +} + +type Query { + firstUser: User1 @http(url: "http://jsonplaceholder.typicode.com/users/1") +} + +type User { + id: Int + name: String +} + +type User1 { + user1: User2 +} + +type User2 { + user2: User @http(url: "http://jsonplaceholder.typicode.com/users/1") } ``` diff --git a/tests/execution/request-to-upstream-batching.md b/tests/execution/request-to-upstream-batching.md index c065cbb881..d9de545e0d 100644 --- a/tests/execution/request-to-upstream-batching.md +++ b/tests/execution/request-to-upstream-batching.md @@ -1,68 +1,22 @@ # Batched graphql request to batched upstream query -```json @config -{ - "server": { - "batchRequests": true - }, - "upstream": { - "batch": { - "maxSize": 100, - "delay": 1, - "headers": [] - } - }, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "args": { - "id": { - "type": { - "name": "Int", - "required": true - } - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users", - "query": [ - { - "key": "id", - "value": "{{.args.id}}" - } - ], - "batchKey": ["id"] - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server(batchRequests: true) @upstream(batch: {maxSize: 100, delay: 1, headers: []}) { + query: Query +} + +type Query { + user(id: Int!): User + @http( + url: "http://jsonplaceholder.typicode.com/users" + query: [{key: "id", value: "{{.args.id}}"}] + batchKey: ["id"] + ) +} + +type User { + id: Int + name: String } ``` diff --git a/tests/execution/simple-query.md b/tests/execution/simple-query.md index f706289a95..557c3b7e88 100644 --- a/tests/execution/simple-query.md +++ b/tests/execution/simple-query.md @@ -1,44 +1,17 @@ # Simple query -```json @config -{ - "server": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "firstUser": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server @upstream { + query: Query +} + +type Query { + firstUser: User @http(url: "http://jsonplaceholder.typicode.com/users/1") +} + +type User { + id: Int + name: String } ``` diff --git a/tests/execution/test-enum-empty.md b/tests/execution/test-enum-empty.md index 66fbac46c2..c1d3ebcf55 100644 --- a/tests/execution/test-enum-empty.md +++ b/tests/execution/test-enum-empty.md @@ -4,42 +4,14 @@ error: true # test-enum-empty -```json @config -{ - "server": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "foo": { - "type": { - "name": "Foo" - }, - "args": { - "val": { - "type": { - "name": "String", - "required": true - } - } - }, - "expr": { - "body": "{{.args.val}}" - }, - "cache": null, - "protected": null - } - }, - "protected": null - } - }, - "enums": { - "Foo": { - "variants": [], - "doc": null - } - } +```graphql @config +schema @server { + query: Query } + +type Query { + foo(val: String!): Foo @expr(body: "{{.args.val}}") +} + +enum Foo {} ``` diff --git a/tests/execution/test-interface-from-json.md b/tests/execution/test-interface-from-json.md deleted file mode 100644 index 9d2e25dbb6..0000000000 --- a/tests/execution/test-interface-from-json.md +++ /dev/null @@ -1,47 +0,0 @@ -# Interfaces defined in json - -```json @config -{ - "schema": { - "query": "Query" - }, - "types": { - "IA": { - "fields": { - "a": { - "type": { - "name": "String" - } - } - } - }, - "B": { - "implements": ["IA"], - "fields": { - "a": { - "type": { - "name": "String" - } - }, - "b": { - "type": { - "name": "String" - } - } - } - }, - "Query": { - "fields": { - "bar": { - "type": { - "name": "B" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/posts" - } - } - } - } - } -} -``` diff --git a/tests/execution/test-static-value.md b/tests/execution/test-static-value.md deleted file mode 100644 index 960a74700e..0000000000 --- a/tests/execution/test-static-value.md +++ /dev/null @@ -1,62 +0,0 @@ -# Static value - -```json @config -{ - "server": {}, - "upstream": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "firstUser": { - "type": { - "name": "User" - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/1" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } -} -``` - -```yml @mock -- request: - method: GET - url: http://jsonplaceholder.typicode.com/users/1 - response: - status: 200 - body: - id: 1 - name: Leanne Graham -``` - -```yml @test -- method: POST - url: http://localhost:8080/graphql - body: - query: query { firstUser { name } } -``` diff --git a/tests/execution/upstream-batching.md b/tests/execution/upstream-batching.md index e65c735567..b911bad51a 100644 --- a/tests/execution/upstream-batching.md +++ b/tests/execution/upstream-batching.md @@ -1,65 +1,22 @@ # Sending requests to be batched by the upstream server -```json @config -{ - "server": {}, - "upstream": { - "batch": { - "maxSize": 100, - "delay": 1, - "headers": [] - } - }, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "args": { - "id": { - "type": { - "name": "Int" - } - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users", - "query": [ - { - "key": "id", - "value": "{{.args.id}}" - } - ], - "batchKey": ["id"] - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server @upstream(batch: {maxSize: 100, delay: 1, headers: []}) { + query: Query +} + +type Query { + user(id: Int): User + @http( + url: "http://jsonplaceholder.typicode.com/users" + query: [{key: "id", value: "{{.args.id}}"}] + batchKey: ["id"] + ) +} + +type User { + id: Int + name: String } ``` diff --git a/tests/execution/with-args-url.md b/tests/execution/with-args-url.md index ccb98f2cc0..0fa10d9b4b 100644 --- a/tests/execution/with-args-url.md +++ b/tests/execution/with-args-url.md @@ -1,52 +1,17 @@ # With args URL -```json @config -{ - "server": {}, - "schema": { - "query": "Query" - }, - "types": { - "Query": { - "fields": { - "user": { - "type": { - "name": "User" - }, - "args": { - "id": { - "type": { - "name": "Int", - "required": true - } - } - }, - "http": { - "url": "http://jsonplaceholder.typicode.com/users/{{.args.id}}" - }, - "cache": null - } - }, - "cache": null - }, - "User": { - "fields": { - "id": { - "type": { - "name": "Int" - }, - "cache": null - }, - "name": { - "type": { - "name": "String" - }, - "cache": null - } - }, - "cache": null - } - } +```graphql @config +schema @server { + query: Query +} + +type Query { + user(id: Int!): User @http(url: "http://jsonplaceholder.typicode.com/users/{{.args.id}}") +} + +type User { + id: Int + name: String } ``` diff --git a/tests/execution/yaml-nested-unions.md b/tests/execution/yaml-nested-unions.md index 2f3dee7431..59b939c0a3 100644 --- a/tests/execution/yaml-nested-unions.md +++ b/tests/execution/yaml-nested-unions.md @@ -1,60 +1,36 @@ # Using union types inside other union types -```yml @config -schema: +```graphql @config +schema { query: Query +} -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true - - T4: - fields: - t4: - type: - name: String - - T5: - fields: - t5: - type: - name: Boolean - - Query: - fields: - test: - type: - name: U - args: - u: - type: - name: U - required: true - http: - url: http://localhost/users/{{args.u}} - -unions: - U1: - types: ["T1", "T2", "T3"] - U2: - types: ["T3", "T4"] - U: - types: ["U1", "U2", "T5"] +type T1 { + t1: String +} + +type T2 { + t2: Int +} + +type T3 { + t3: Boolean + t33: Float! +} + +type T4 { + t4: String +} + +type T5 { + t5: Boolean +} + +union U1 = T1 | T2 | T3 +union U2 = T3 | T4 +union U = U1 | U2 | T5 + +type Query { + test(u: U!): U @http(url: "http://localhost/users/{{args.u}}") +} ``` diff --git a/tests/execution/yaml-union-in-type.md b/tests/execution/yaml-union-in-type.md index 2074c69945..8a56322500 100644 --- a/tests/execution/yaml-union-in-type.md +++ b/tests/execution/yaml-union-in-type.md @@ -1,68 +1,37 @@ # Using Union types inside usual type -```yml @config -schema: +```graphql @config +schema { query: Query +} -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true +type T1 { + t1: String +} - NU: - fields: - test: - type: - name: String - u: - type: - name: U +type T2 { + t2: Int +} - NNU: - fields: - other: - type: - name: Int - new: - type: - name: Boolean - nu: - type: - name: NU +type T3 { + t3: Boolean + t33: Float! +} - Query: - fields: - test: - type: - name: U - args: - nu: - type: - name: NU - required: true - nnu: - type: - name: NNU - http: - url: http://localhost/users/{{args.nu.u}} +type NU { + test: String + u: U +} -unions: - U: - types: ["T1", "T2", "T3"] +type NNU { + other: Int + new: Boolean + nu: NU +} + +union U = T1 | T2 | T3 + +type Query { + test(nu: NU!, nnu: NNU): U @http(url: "http://localhost/users/{{args.nu.u}}") +} ``` diff --git a/tests/execution/yaml-union.md b/tests/execution/yaml-union.md index e15d9eeec9..ec31ed9073 100644 --- a/tests/execution/yaml-union.md +++ b/tests/execution/yaml-union.md @@ -1,54 +1,34 @@ # Using Union types in yaml config -```yml @config -schema: +```graphql @config +schema { query: Query +} -types: - T1: - fields: - t1: - type: - name: String - T2: - fields: - t2: - type: - name: Int - T3: - fields: - t3: - type: - name: Boolean - t33: - type: - name: Float - required: true - NU: - fields: - u: - type: - name: U - - NNU: - fields: - nu: - type: - name: NU - Query: - fields: - test: - type: - name: U - args: - u: - type: - name: U - required: true - http: - url: http://localhost/users/{{args.u}}/ - -unions: - U: - types: ["T1", "T2", "T3"] +type T1 { + t1: String +} + +type T2 { + t2: Int +} + +type T3 { + t3: Boolean + t33: Float! +} + +type NU { + u: U +} + +type NNU { + nu: NU +} + +union U = T1 | T2 | T3 + +type Query { + test(u: U!): U @http(url: "http://localhost/users/{{args.u}}/") +} ``` diff --git a/tests/graphql_spec.graphql b/tests/graphql_spec.graphql deleted file mode 100644 index 6f57f54ad0..0000000000 --- a/tests/graphql_spec.graphql +++ /dev/null @@ -1 +0,0 @@ -directive @error(message: String!, trace: [String!]!, description: String) repeatable on OBJECT From 626b1427336dd170857e9bc94fb578685899cd6b Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:48:17 +0100 Subject: [PATCH 15/16] chore: cleanup json/yaml support for cli (#3217) --- examples/generate.yml | 4 +- examples/jsonplaceholder-generated.graphql | 78 +++++++++++++++++++ src/cli/command.rs | 6 -- src/cli/generator/config.rs | 15 +--- src/cli/generator/generator.rs | 4 +- src/cli/tc/check.rs | 7 +- src/cli/tc/run.rs | 4 +- .../fixtures/generator/simple-json.json | 36 --------- tests/cli/fixtures/generator/gen_deezer.md | 3 +- .../generator/gen_json_proto_mix_config.md | 3 +- .../fixtures/generator/gen_jsonplaceholder.md | 3 +- .../gen_proto_with_proto_paths_config.md | 3 +- .../fixtures/generator/proto-connect-rpc.md | 3 +- 13 files changed, 93 insertions(+), 76 deletions(-) create mode 100644 examples/jsonplaceholder-generated.graphql delete mode 100644 tailcall-fixtures/fixtures/generator/simple-json.json diff --git a/examples/generate.yml b/examples/generate.yml index 13b1a909ea..ff353659e6 100644 --- a/examples/generate.yml +++ b/examples/generate.yml @@ -37,11 +37,9 @@ inputs: fieldName: postComments preset: mergeType: 1 - consolidateURL: 0.5 treeShake: true inferTypeNames: true output: - path: "./jsonplaceholder.graphql" - format: graphQL + path: "./jsonplaceholder-generated.graphql" schema: query: Query diff --git a/examples/jsonplaceholder-generated.graphql b/examples/jsonplaceholder-generated.graphql new file mode 100644 index 0000000000..9236d733c4 --- /dev/null +++ b/examples/jsonplaceholder-generated.graphql @@ -0,0 +1,78 @@ +schema @server @upstream(allowedHeaders: ["Accept", "Content-Type"]) { + query: Query +} + +type Address { + city: String + geo: Geo + street: String + suite: String + zipcode: String +} + +type Comment { + body: String + email: String + id: Int + name: String + postId: Int +} + +type Company { + bs: String + catchPhrase: String + name: String +} + +type Geo { + lat: String + lng: String +} + +type Photo { + albumId: Int + id: Int + thumbnailUrl: String + title: String + url: String +} + +type Post { + body: String + id: Int + title: String + userId: Int +} + +type Query { + comment(GEN__1: Int!): Comment @http(url: "https://jsonplaceholder.typicode.com/comments/{{.args.GEN__1}}") + comments: [Comment] @http(url: "https://jsonplaceholder.typicode.com/comments") + photo(GEN__1: Int!): Photo @http(url: "https://jsonplaceholder.typicode.com/photos/{{.args.GEN__1}}") + photos: [Photo] @http(url: "https://jsonplaceholder.typicode.com/photos") + post(GEN__1: Int!): Post @http(url: "https://jsonplaceholder.typicode.com/posts/{{.args.GEN__1}}") + postComments(postId: Int): [Comment] + @http(url: "https://jsonplaceholder.typicode.com/comments", query: [{key: "postId", value: "{{.args.postId}}"}]) + posts: [Post] @http(url: "https://jsonplaceholder.typicode.com/posts") + todo(GEN__1: Int!): Todo @http(url: "https://jsonplaceholder.typicode.com/todos/{{.args.GEN__1}}") + todos: [Todo] @http(url: "https://jsonplaceholder.typicode.com/todos") + user(GEN__1: Int!): User @http(url: "https://jsonplaceholder.typicode.com/users/{{.args.GEN__1}}") + users: [User] @http(url: "https://jsonplaceholder.typicode.com/users") +} + +type Todo { + completed: Boolean + id: Int + title: String + userId: Int +} + +type User { + address: Address + company: Company + email: String + id: Int + name: String + phone: String + username: String + website: String +} diff --git a/src/cli/command.rs b/src/cli/command.rs index cb62f4816e..0f1849a67e 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -2,8 +2,6 @@ use clap::{Parser, Subcommand}; use strum_macros::Display; use tailcall_version::VERSION; -use crate::core::config; - const ABOUT: &str = r" __ _ __ ____ / /_____ _(_) /________ _/ / / @@ -49,10 +47,6 @@ pub enum Command { #[arg(short, long)] schema: bool, - /// Prints the input config in the provided format - #[clap(short, long)] - format: Option, - /// Controls SSL/TLS certificate verification for remote config files /// Set to false to skip certificate verification (not recommended for /// production) diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index 1e6e8f119c..b753ab9c46 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -11,7 +11,6 @@ use tailcall_valid::{Valid, ValidateFrom, Validator}; use url::Url; use crate::core::config::transformer::Preset; -use crate::core::config::{self}; use crate::core::http::Method; #[derive(Deserialize, Serialize, Debug, Default, Setters)] @@ -102,8 +101,6 @@ pub enum Source { pub struct Output { #[serde(skip_serializing_if = "Location::is_empty")] pub path: Location, - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, } #[derive(Debug)] @@ -202,10 +199,7 @@ impl Headers { impl Output { pub fn resolve(self, parent_dir: Option<&Path>) -> anyhow::Result> { - Ok(Output { - format: self.format, - path: self.path.into_resolved(parent_dir), - }) + Ok(Output { path: self.path.into_resolved(parent_dir) }) } } @@ -451,10 +445,9 @@ mod tests { let json = r#" {"output": { "paths": "./output.graphql", - }} + }} "#; - let expected_error = - "unknown field `paths`, expected `path` or `format` at line 3 column 21"; + let expected_error = "unknown field `paths`, expected `path` at line 3 column 21"; assert_deserialization_error(json, expected_error); } @@ -463,7 +456,7 @@ mod tests { let json = r#" {"schema": { "querys": "Query", - }} + }} "#; let expected_error = "unknown field `querys`, expected `query` or `mutation` at line 3 column 22"; diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index 94ef7319cd..3fa3165396 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -1,6 +1,7 @@ use std::fs; use std::path::Path; +use anyhow::anyhow; use http::header::{HeaderMap, HeaderName, HeaderValue}; use inquire::Confirm; use pathdiff::diff_paths; @@ -34,9 +35,8 @@ impl Generator { async fn write(self, graphql_config: &ConfigModule, output_path: &str) -> anyhow::Result<()> { let output_source = config::Source::detect(output_path)?; let config = match output_source { - config::Source::Json => graphql_config.to_json(true)?, - config::Source::Yml => graphql_config.to_yaml()?, config::Source::GraphQL => graphql_config.to_sdl(), + _ => return Err(anyhow!("Only graphql output format is currently supported")), }; if self.should_overwrite(output_path)? { diff --git a/src/cli/tc/check.rs b/src/cli/tc/check.rs index 6816836092..9aca36c783 100644 --- a/src/cli/tc/check.rs +++ b/src/cli/tc/check.rs @@ -4,7 +4,6 @@ use super::helpers::{display_schema, log_endpoint_set}; use crate::cli::fmt::Fmt; use crate::core::blueprint::Blueprint; use crate::core::config::reader::ConfigReader; -use crate::core::config::Source; use crate::core::runtime::TargetRuntime; use crate::core::Errata; @@ -12,18 +11,14 @@ pub(super) struct CheckParams { pub(super) file_paths: Vec, pub(super) n_plus_one_queries: bool, pub(super) schema: bool, - pub(super) format: Option, pub(super) runtime: TargetRuntime, } pub(super) async fn check_command(params: CheckParams, config_reader: &ConfigReader) -> Result<()> { - let CheckParams { file_paths, n_plus_one_queries, schema, format, runtime } = params; + let CheckParams { file_paths, n_plus_one_queries, schema, runtime } = params; let config_module = (config_reader.read_all(&file_paths)).await?; log_endpoint_set(&config_module.extensions().endpoint_set); - if let Some(format) = format { - Fmt::display(format.encode(&config_module)?); - } let blueprint = Blueprint::try_from(&config_module).map_err(Errata::from); match blueprint { diff --git a/src/cli/tc/run.rs b/src/cli/tc/run.rs index 61f93f4394..ecb8d2a55f 100644 --- a/src/cli/tc/run.rs +++ b/src/cli/tc/run.rs @@ -46,11 +46,11 @@ async fn run_command(cli: Cli) -> Result<()> { validate_rc_config_files(runtime, &file_paths).await; start::start_command(file_paths, &config_reader).await?; } - Command::Check { file_paths, n_plus_one_queries, schema, format, verify_ssl } => { + Command::Check { file_paths, n_plus_one_queries, schema, verify_ssl } => { let (runtime, config_reader) = get_runtime_and_config_reader(verify_ssl); validate_rc_config_files(runtime.clone(), &file_paths).await; check::check_command( - check::CheckParams { file_paths, n_plus_one_queries, schema, format, runtime }, + check::CheckParams { file_paths, n_plus_one_queries, schema, runtime }, &config_reader, ) .await?; diff --git a/tailcall-fixtures/fixtures/generator/simple-json.json b/tailcall-fixtures/fixtures/generator/simple-json.json deleted file mode 100644 index 9e1d8f61cf..0000000000 --- a/tailcall-fixtures/fixtures/generator/simple-json.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "inputs": [ - { - "curl": { - "src": "https://jsonplaceholder.typicode.com/posts/1", - "headers": { - "Content-Type": "application/json", - "Accept": "application/json" - }, - "fieldName": "post" - } - }, - { - "curl": { - "src": "https://jsonplaceholder.typicode.com/posts", - "fieldName": "posts" - } - }, - { - "proto": { - "src": "../protobuf/news.proto" - } - } - ], - "preset": { - "mergeType": 1.0, - "consolidateURL": 0.5 - }, - "output": { - "path": "./output.graphql", - "format": "graphQL" - }, - "schema": { - "query": "Query" - } -} diff --git a/tests/cli/fixtures/generator/gen_deezer.md b/tests/cli/fixtures/generator/gen_deezer.md index 744b7e1bc8..3dc19e88d3 100644 --- a/tests/cli/fixtures/generator/gen_deezer.md +++ b/tests/cli/fixtures/generator/gen_deezer.md @@ -56,8 +56,7 @@ "inferTypeNames": true }, "output": { - "path": "./output.graphql", - "format": "graphQL" + "path": "./output.graphql" }, "schema": { "query": "Query" diff --git a/tests/cli/fixtures/generator/gen_json_proto_mix_config.md b/tests/cli/fixtures/generator/gen_json_proto_mix_config.md index 5fe595e5f0..42a8406490 100644 --- a/tests/cli/fixtures/generator/gen_json_proto_mix_config.md +++ b/tests/cli/fixtures/generator/gen_json_proto_mix_config.md @@ -20,8 +20,7 @@ "treeShake": true }, "output": { - "path": "./output.graphql", - "format": "graphQL" + "path": "./output.graphql" }, "schema": { "query": "Query" diff --git a/tests/cli/fixtures/generator/gen_jsonplaceholder.md b/tests/cli/fixtures/generator/gen_jsonplaceholder.md index 9c3066ff49..d1f6335ea2 100644 --- a/tests/cli/fixtures/generator/gen_jsonplaceholder.md +++ b/tests/cli/fixtures/generator/gen_jsonplaceholder.md @@ -78,8 +78,7 @@ "inferTypeNames": true }, "output": { - "path": "./output.graphql", - "format": "graphQL" + "path": "./output.graphql" }, "schema": { "query": "Query" diff --git a/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md b/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md index 618da86ab5..5b508368e3 100644 --- a/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md +++ b/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md @@ -21,8 +21,7 @@ "treeShake": true }, "output": { - "path": "./output.graphql", - "format": "graphQL" + "path": "./output.graphql" }, "schema": { "query": "Query" diff --git a/tests/cli/fixtures/generator/proto-connect-rpc.md b/tests/cli/fixtures/generator/proto-connect-rpc.md index cd16f7bc2c..8bbe9bde78 100644 --- a/tests/cli/fixtures/generator/proto-connect-rpc.md +++ b/tests/cli/fixtures/generator/proto-connect-rpc.md @@ -21,8 +21,7 @@ "treeShake": true }, "output": { - "path": "./output.graphql", - "format": "graphQL" + "path": "./output.graphql" }, "schema": { "query": "Query" From f067629b7f63e843048a38a542a8734d90d8aaa8 Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:49:05 +0530 Subject: [PATCH 16/16] feat: enable POST requests batching with dl. (#3140) Co-authored-by: Tushar Mathur --- Cargo.toml | 1 - generated/.tailcallrc.graphql | 12 +- src/core/blueprint/error.rs | 10 +- src/core/blueprint/operators/http.rs | 107 ++++++-- ...lueprint__index__test__from_blueprint.snap | 8 +- src/core/config/directives/http.rs | 5 +- src/core/config/transformer/subgraph.rs | 10 +- src/core/endpoint.rs | 2 +- .../generator/json/operation_generator.rs | 5 +- src/core/generator/proto/connect_rpc.rs | 9 +- src/core/http/data_loader.rs | 147 ++++++----- src/core/http/data_loader_request.rs | 42 ++- src/core/http/mod.rs | 1 + src/core/http/request_template.rs | 146 ++++++++--- .../http/transformations/body_batching.rs | 248 ++++++++++++++++++ src/core/http/transformations/mod.rs | 5 + .../http/transformations/query_batching.rs | 200 ++++++++++++++ src/core/ir/eval_http.rs | 32 ++- src/core/ir/eval_io.rs | 8 +- src/core/ir/mod.rs | 2 + src/core/ir/request.rs | 40 +++ src/core/json/borrow.rs | 8 + src/core/json/graphql.rs | 8 + src/core/json/json_like.rs | 4 +- src/core/json/serde.rs | 8 + src/core/worker/worker.rs | 10 + ...ures__generator__proto-connect-rpc.md.snap | 12 +- .../batching-validation.md_error.snap | 45 ++++ .../snapshots/body-batching-cases.md_0.snap | 34 +++ .../snapshots/body-batching-cases.md_1.snap | 32 +++ .../snapshots/body-batching-cases.md_2.snap | 32 +++ .../body-batching-cases.md_client.snap | 40 +++ .../body-batching-cases.md_merged.snap | 53 ++++ tests/core/snapshots/body-batching.md_0.snap | 43 +++ tests/core/snapshots/body-batching.md_1.snap | 40 +++ .../snapshots/body-batching.md_client.snap | 29 ++ .../snapshots/body-batching.md_merged.snap | 43 +++ .../test-batch-operator-post.md_error.snap | 2 +- tests/execution/batching-validation.md | 47 ++++ tests/execution/body-batching-cases.md | 154 +++++++++++ tests/execution/body-batching.md | 114 ++++++++ tests/execution/call-mutation.md | 2 +- 42 files changed, 1622 insertions(+), 178 deletions(-) create mode 100644 src/core/http/transformations/body_batching.rs create mode 100644 src/core/http/transformations/mod.rs create mode 100644 src/core/http/transformations/query_batching.rs create mode 100644 src/core/ir/request.rs create mode 100644 tests/core/snapshots/batching-validation.md_error.snap create mode 100644 tests/core/snapshots/body-batching-cases.md_0.snap create mode 100644 tests/core/snapshots/body-batching-cases.md_1.snap create mode 100644 tests/core/snapshots/body-batching-cases.md_2.snap create mode 100644 tests/core/snapshots/body-batching-cases.md_client.snap create mode 100644 tests/core/snapshots/body-batching-cases.md_merged.snap create mode 100644 tests/core/snapshots/body-batching.md_0.snap create mode 100644 tests/core/snapshots/body-batching.md_1.snap create mode 100644 tests/core/snapshots/body-batching.md_client.snap create mode 100644 tests/core/snapshots/body-batching.md_merged.snap create mode 100644 tests/execution/batching-validation.md create mode 100644 tests/execution/body-batching-cases.md create mode 100644 tests/execution/body-batching.md diff --git a/Cargo.toml b/Cargo.toml index 199d15005d..b47ae50314 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -248,7 +248,6 @@ default = ["cli", "js"] # Feature flag to force JIT engine inside integration tests force_jit = [] - [workspace] members = [ ".", diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index 9f6f170ea6..587bcd53c7 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -173,10 +173,10 @@ directive @http( batchKey: [String!] """ The body of the API call. It's used for methods like POST or PUT that send data to - the server. You can pass it as a static object or use a Mustache template to substitute - variables from the GraphQL variables. + the server. You can pass it as a static object or use a Mustache template with object + to substitute variables from the GraphQL variables. """ - body: String + body: JSON """ Enables deduplication of IO operations to enhance performance.This flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: @@ -918,10 +918,10 @@ input Http { batchKey: [String!] """ The body of the API call. It's used for methods like POST or PUT that send data to - the server. You can pass it as a static object or use a Mustache template to substitute - variables from the GraphQL variables. + the server. You can pass it as a static object or use a Mustache template with object + to substitute variables from the GraphQL variables. """ - body: String + body: JSON """ Enables deduplication of IO operations to enhance performance.This flag prevents duplicate IO requests from being executed concurrently, reducing resource load. Caution: diff --git a/src/core/blueprint/error.rs b/src/core/blueprint/error.rs index a8980ac8e7..4d4dd6337b 100644 --- a/src/core/blueprint/error.rs +++ b/src/core/blueprint/error.rs @@ -46,12 +46,18 @@ pub enum BlueprintError { #[error("Protobuf files were not specified in the config")] ProtobufFilesNotSpecifiedInConfig, - #[error("GroupBy is only supported for GET requests")] - GroupByOnlyForGet, + #[error("GroupBy is only supported for GET and POST requests")] + GroupByOnlyForGetAndPost, + + #[error("Request body batching requires exactly one dynamic value in the body.")] + BatchRequiresDynamicParameter, #[error("Batching capability was used without enabling it in upstream")] IncorrectBatchingUsage, + #[error("batchKey requires either body or query parameters")] + BatchKeyRequiresEitherBodyOrQuery, + #[error("script is required")] ScriptIsRequired, diff --git a/src/core/blueprint/operators/http.rs b/src/core/blueprint/operators/http.rs index cfc8ca879f..ab25a25ced 100644 --- a/src/core/blueprint/operators/http.rs +++ b/src/core/blueprint/operators/http.rs @@ -22,15 +22,11 @@ pub fn compile_http( Err(e) => Valid::from_validation_err(BlueprintError::from_validation_string(e)), }; - Valid::<(), BlueprintError>::fail(BlueprintError::GroupByOnlyForGet) - .when(|| !http.batch_key.is_empty() && http.method != Method::GET) - .and( - Valid::<(), BlueprintError>::fail(BlueprintError::IncorrectBatchingUsage).when(|| { - (config_module.upstream.get_delay() < 1 - || config_module.upstream.get_max_size() < 1) - && !http.batch_key.is_empty() - }), - ) + Valid::<(), BlueprintError>::fail(BlueprintError::IncorrectBatchingUsage) + .when(|| { + (config_module.upstream.get_delay() < 1 || config_module.upstream.get_max_size() < 1) + && !http.batch_key.is_empty() + }) .and( Valid::from_iter(http.query.iter(), |query| { validate_argument(config_module, Mustache::parse(query.value.as_str()), field) @@ -38,6 +34,12 @@ pub fn compile_http( .unit() .trace("query"), ) + .and( + Valid::<(), BlueprintError>::fail(BlueprintError::BatchKeyRequiresEitherBodyOrQuery) + .when(|| { + !http.batch_key.is_empty() && (http.body.is_none() && http.query.is_empty()) + }), + ) .and(Valid::succeed(http.url.as_str())) .zip(mustache_headers) .and_then(|(base_url, headers)| { @@ -67,6 +69,22 @@ pub fn compile_http( Err(e) => Valid::fail(BlueprintError::Error(e)), } }) + .and_then(|request_template| { + if !http.batch_key.is_empty() && (http.body.is_some() || http.method != Method::GET) { + if let Some(body) = http.body.as_ref() { + let dynamic_paths = count_dynamic_paths(body); + if dynamic_paths != 1 { + Valid::fail(BlueprintError::BatchRequiresDynamicParameter).trace("body") + } else { + Valid::succeed(request_template) + } + } else { + Valid::fail(BlueprintError::BatchRequiresDynamicParameter).trace("body") + } + } else { + Valid::succeed(request_template) + } + }) .map(|req_template| { // marge http and upstream on_request let on_request = http @@ -76,13 +94,18 @@ pub fn compile_http( let on_response_body = http.on_response_body.clone(); let hook = WorkerHooks::try_new(on_request, on_response_body).ok(); - let io = if !http.batch_key.is_empty() && http.method == Method::GET { + let io = if !http.batch_key.is_empty() { // Find a query parameter that contains a reference to the {{.value}} key - let key = http.query.iter().find_map(|q| { - Mustache::parse(&q.value) - .expression_contains("value") - .then(|| q.key.clone()) - }); + let key = if http.method == Method::GET { + http.query.iter().find_map(|q| { + Mustache::parse(&q.value) + .expression_contains("value") + .then(|| q.key.clone()) + }) + } else { + None + }; + IR::IO(IO::Http { req_template, group_by: Some(GroupBy::new(http.batch_key.clone(), key)), @@ -105,3 +128,57 @@ pub fn compile_http( }) .and_then(apply_select) } + +/// Count the number of dynamic expressions in the JSON value. +fn count_dynamic_paths(json: &serde_json::Value) -> usize { + let mut count = 0; + match json { + serde_json::Value::Array(arr) => { + for v in arr { + count += count_dynamic_paths(v) + } + } + serde_json::Value::Object(obj) => { + for (_, v) in obj { + count += count_dynamic_paths(v) + } + } + serde_json::Value::String(s) => { + if !Mustache::parse(s).is_const() { + count += 1; + } + } + _ => {} + } + count +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use super::*; + + #[test] + fn test_extract_expression_keys_from_nested_objects() { + let json = r#"{"body":"d","userId":"{{.value.uid}}","nested":{"other":"{{test}}"}}"#; + let json = serde_json::from_str(json).unwrap(); + let keys = count_dynamic_paths(&json); + assert_eq!(keys, 2); + } + + #[test] + fn test_extract_expression_keys_from_mixed_json() { + let json = r#"{"body":"d","userId":"{{.value.uid}}","nested":{"other":"{{test}}"},"meta":[{"key": "id", "value": "{{.value.userId}}"}]}"#; + let json = serde_json::from_str(json).unwrap(); + let keys = count_dynamic_paths(&json); + assert_eq!(keys, 3); + } + + #[test] + fn test_with_non_json_value() { + let json = json!(r#"{{.value}}"#); + let keys = count_dynamic_paths(&json); + assert_eq!(keys, 1); + } +} diff --git a/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap b/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap index 33dd769568..7336ccd0cf 100644 --- a/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap +++ b/src/core/blueprint/snapshots/tailcall__core__blueprint__index__test__from_blueprint.snap @@ -58,7 +58,7 @@ Index { ), headers: {}, body: Some( - "{{.args.input}}", + String("{{.args.input}}"), ), description: None, encoding: ApplicationJson, @@ -127,7 +127,7 @@ Index { ), headers: {}, body: Some( - "{{.args.input}}", + String("{{.args.input}}"), ), description: None, encoding: ApplicationJson, @@ -205,7 +205,7 @@ Index { ), headers: {}, body: Some( - "{{.args.input}}", + String("{{.args.input}}"), ), description: None, encoding: ApplicationJson, @@ -286,7 +286,7 @@ Index { ), headers: {}, body: Some( - "{{.args.input}}", + String("{{.args.input}}"), ), description: None, encoding: ApplicationJson, diff --git a/src/core/config/directives/http.rs b/src/core/config/directives/http.rs index 9aec5e73be..150f871d8d 100644 --- a/src/core/config/directives/http.rs +++ b/src/core/config/directives/http.rs @@ -40,8 +40,9 @@ pub struct Http { #[serde(default, skip_serializing_if = "is_default")] /// The body of the API call. It's used for methods like POST or PUT that /// send data to the server. You can pass it as a static object or use a - /// Mustache template to substitute variables from the GraphQL variables. - pub body: Option, + /// Mustache template with object to substitute variables from the GraphQL + /// variables. + pub body: Option, #[serde(default, skip_serializing_if = "is_default")] /// The `encoding` parameter specifies the encoding of the request body. It diff --git a/src/core/config/transformer/subgraph.rs b/src/core/config/transformer/subgraph.rs index 24eed1f86c..0d336d544f 100644 --- a/src/core/config/transformer/subgraph.rs +++ b/src/core/config/transformer/subgraph.rs @@ -275,7 +275,7 @@ impl KeysExtractor { Valid::from_iter( [ Self::parse_str(http.url.as_str()).trace("url"), - Self::parse_str_option(http.body.as_deref()).trace("body"), + Self::parse_json_option(http.body.as_ref()).trace("body"), Self::parse_key_value_iter(http.headers.iter()).trace("headers"), Self::parse_key_value_iter(http.query.iter().map(|q| KeyValue { key: q.key.to_string(), @@ -355,9 +355,9 @@ impl KeysExtractor { .map_to(keys) } - fn parse_str_option(s: Option<&str>) -> Valid { + fn parse_json_option(s: Option<&serde_json::Value>) -> Valid { if let Some(s) = s { - Self::parse_str(s) + Self::parse_str(&s.to_string()) } else { Valid::succeed(Keys::new()) } @@ -483,7 +483,9 @@ mod tests { fn test_extract_http() { let http = Http { url: "http://tailcall.run/users/{{.value.id}}".to_string(), - body: Some(r#"{ "obj": "{{.value.obj}}"} "#.to_string()), + body: Some(serde_json::Value::String( + r#"{ "obj": "{{.value.obj}}"} "#.to_string(), + )), headers: vec![KeyValue { key: "{{.value.header.key}}".to_string(), value: "{{.value.header.value}}".to_string(), diff --git a/src/core/endpoint.rs b/src/core/endpoint.rs index d809096779..6099acec54 100644 --- a/src/core/endpoint.rs +++ b/src/core/endpoint.rs @@ -13,7 +13,7 @@ pub struct Endpoint { pub input: JsonSchema, pub output: JsonSchema, pub headers: HeaderMap, - pub body: Option, + pub body: Option, pub description: Option, pub encoding: Encoding, } diff --git a/src/core/generator/json/operation_generator.rs b/src/core/generator/json/operation_generator.rs index fe0aeb36aa..79bde56e71 100644 --- a/src/core/generator/json/operation_generator.rs +++ b/src/core/generator/json/operation_generator.rs @@ -41,7 +41,10 @@ impl OperationTypeGenerator { let arg_name_gen = NameGenerator::new(prefix.as_str()); let arg_name = arg_name_gen.next(); - http_resolver.body = Some(format!("{{{{.args.{}}}}}", arg_name)); + http_resolver.body = Some(serde_json::Value::String(format!( + "{{{{.args.{}}}}}", + arg_name + ))); http_resolver.method = request_sample.method.to_owned(); field.args.insert( diff --git a/src/core/generator/proto/connect_rpc.rs b/src/core/generator/proto/connect_rpc.rs index 4363631e2c..14fb51243b 100644 --- a/src/core/generator/proto/connect_rpc.rs +++ b/src/core/generator/proto/connect_rpc.rs @@ -55,7 +55,7 @@ impl From for Http { Self { url: new_url, - body: body.map(|b| b.to_string()), + body, method: crate::core::http::Method::POST, headers, batch_key, @@ -91,7 +91,7 @@ mod tests { assert_eq!(http.url, "http://localhost:8080/package.service/method"); assert_eq!(http.method, crate::core::http::Method::POST); - assert_eq!(http.body, Some(r#"{"key":"value"}"#.to_string())); + assert_eq!(http.body, Some(json!({"key": "value"}))); } #[test] @@ -109,7 +109,7 @@ mod tests { let http = Http::from(grpc); - assert_eq!(http.body, Some("{}".to_string())); + assert_eq!(http.body, Some(json!({}))); } #[test] @@ -136,6 +136,7 @@ mod tests { .value, "bar".to_string() ); + assert_eq!(http.body, Some(json!({}))); } #[test] @@ -155,7 +156,7 @@ mod tests { assert_eq!(http.url, "http://localhost:8080/package.service/method"); assert_eq!(http.method, crate::core::http::Method::POST); - assert_eq!(http.body, Some(r#"{"key":"value"}"#.to_string())); + assert_eq!(http.body, Some(json!({"key": "value"}))); assert_eq!( http.headers .iter() diff --git a/src/core/http/data_loader.rs b/src/core/http/data_loader.rs index c5443d93fd..72d72c4a50 100644 --- a/src/core/http/data_loader.rs +++ b/src/core/http/data_loader.rs @@ -5,13 +5,17 @@ use std::time::Duration; use async_graphql::async_trait; use async_graphql::futures_util::future::join_all; use async_graphql_value::ConstValue; +use tailcall_valid::Validator; +use super::transformations::{BodyBatching, QueryBatching}; use crate::core::config::group_by::GroupBy; use crate::core::config::Batch; use crate::core::data_loader::{DataLoader, Loader}; use crate::core::http::{DataLoaderRequest, Response}; use crate::core::json::JsonLike; use crate::core::runtime::TargetRuntime; +use crate::core::transform::TransformerOps; +use crate::core::Transform; fn get_body_value_single(body_value: &HashMap>, id: &str) -> ConstValue { body_value @@ -35,19 +39,11 @@ fn get_body_value_list(body_value: &HashMap>, id: &str) pub struct HttpDataLoader { pub runtime: TargetRuntime, pub group_by: Option, - pub body: fn(&HashMap>, &str) -> ConstValue, + is_list: bool, } impl HttpDataLoader { pub fn new(runtime: TargetRuntime, group_by: Option, is_list: bool) -> Self { - HttpDataLoader { - runtime, - group_by, - body: if is_list { - get_body_value_list - } else { - get_body_value_single - }, - } + HttpDataLoader { runtime, group_by, is_list } } pub fn to_data_loader(self, batch: Batch) -> DataLoader { @@ -69,61 +65,86 @@ impl Loader for HttpDataLoader { if let Some(group_by) = &self.group_by { let query_name = group_by.key(); let mut dl_requests = keys.to_vec(); - - // Sort keys to build consistent URLs - // TODO: enable in tests only - dl_requests.sort_by(|a, b| a.to_request().url().cmp(b.to_request().url())); - - // Create base request - let mut request = dl_requests[0].to_request(); - let first_url = request.url_mut(); - - // Merge query params in the request - for key in &dl_requests[1..] { - let request = key.to_request(); - let url = request.url(); - let pairs: Vec<_> = url - .query_pairs() - .filter(|(key, _)| group_by.key().eq(&key.to_string())) - .collect(); - first_url.query_pairs_mut().extend_pairs(pairs); + if cfg!(debug_assertions) { + // Sort keys to build consistent URLs only in Testing environment. + dl_requests.sort_by(|a, b| a.to_request().url().cmp(b.to_request().url())); } - // Dispatch request - let res = self - .runtime - .http - .execute(request) - .await? - .to_json::()?; - - // Create a response HashMap - #[allow(clippy::mutable_key_type)] - let mut hashmap = HashMap::with_capacity(dl_requests.len()); - - // Parse the response body and group it by batchKey - let path = &group_by.path(); - - // ResponseMap contains the response body grouped by the batchKey - let response_map = res.body.group_by(path); - - // For each request and insert its corresponding value - for dl_req in dl_requests.iter() { - let url = dl_req.url(); - let query_set: HashMap<_, _> = url.query_pairs().collect(); - let id = query_set.get(query_name).ok_or(anyhow::anyhow!( - "Unable to find key {} in query params", - query_name - ))?; - - // Clone the response and set the body - let body = (self.body)(&response_map, id); - let res = res.clone().body(body); - - hashmap.insert(dl_req.clone(), res); + if let Some(base_dl_request) = dl_requests.first().as_mut() { + let base_request = if base_dl_request.method() == http::Method::GET { + QueryBatching::new( + &dl_requests.iter().skip(1).collect::>(), + Some(group_by.key()), + ) + .transform(base_dl_request.to_request()) + .to_result() + .map_err(|e| anyhow::anyhow!(e))? + } else { + QueryBatching::new(&dl_requests.iter().skip(1).collect::>(), None) + .pipe(BodyBatching::new(&dl_requests.iter().collect::>())) + .transform(base_dl_request.to_request()) + .to_result() + .map_err(|e| anyhow::anyhow!(e))? + }; + + // Dispatch request + let res = self + .runtime + .http + .execute(base_request) + .await? + .to_json::()?; + + // Create a response HashMap + #[allow(clippy::mutable_key_type)] + let mut hashmap = HashMap::with_capacity(dl_requests.len()); + + // Parse the response body and group it by batchKey + let path = &group_by.path(); + + // ResponseMap contains the response body grouped by the batchKey + let response_map = res.body.group_by(path); + + // depending on graphql type, it will extract the data out of the response. + let data_extractor = if self.is_list { + get_body_value_list + } else { + get_body_value_single + }; + + // For each request and insert its corresponding value + if base_dl_request.method() == reqwest::Method::GET { + for dl_req in dl_requests.iter() { + let url = dl_req.url(); + let query_set: HashMap<_, _> = url.query_pairs().collect(); + let id = query_set.get(query_name).ok_or(anyhow::anyhow!( + "Unable to find key {} in query params", + query_name + ))?; + + // Clone the response and set the body + let body = data_extractor(&response_map, id); + let res = res.clone().body(body); + + hashmap.insert(dl_req.clone(), res); + } + } else { + for dl_req in dl_requests.into_iter() { + let body_key = dl_req.batching_value().ok_or(anyhow::anyhow!( + "Unable to find batching value in the body for data loader request {}", + dl_req.url().as_str() + ))?; + let extracted_value = data_extractor(&response_map, body_key); + let res = res.clone().body(extracted_value); + hashmap.insert(dl_req.clone(), res); + } + } + + Ok(hashmap) + } else { + let error_message = "This is definitely a bug in http data loaders, please report it to the maintainers."; + Err(anyhow::anyhow!(error_message).into()) } - - Ok(hashmap) } else { let results = keys.iter().map(|key| async { let result = self.runtime.http.execute(key.to_request()).await; @@ -133,7 +154,7 @@ impl Loader for HttpDataLoader { let results = join_all(results).await; #[allow(clippy::mutable_key_type)] - let mut hashmap = HashMap::new(); + let mut hashmap = HashMap::with_capacity(results.len()); for (key, value) in results { hashmap.insert(key, value?.to_json()?); } diff --git a/src/core/http/data_loader_request.rs b/src/core/http/data_loader_request.rs index b386f8daea..4631fdd8a0 100644 --- a/src/core/http/data_loader_request.rs +++ b/src/core/http/data_loader_request.rs @@ -5,34 +5,48 @@ use std::ops::Deref; use tailcall_hasher::TailcallHasher; #[derive(Debug)] -pub struct DataLoaderRequest(reqwest::Request, BTreeSet); +pub struct DataLoaderRequest { + request: reqwest::Request, + headers: BTreeSet, + /// used for request body batching. + batching_value: Option, +} impl DataLoaderRequest { pub fn new(req: reqwest::Request, headers: BTreeSet) -> Self { // TODO: req should already have headers builtin, no? - DataLoaderRequest(req, headers) + Self { request: req, headers, batching_value: None } + } + + pub fn with_batching_value(self, body: Option) -> Self { + Self { batching_value: body, ..self } } + + pub fn batching_value(&self) -> Option<&String> { + self.batching_value.as_ref() + } + pub fn to_request(&self) -> reqwest::Request { // TODO: excessive clone for the whole structure instead of cloning only part of // it check if we really need to clone anything at all or just pass // references? - self.clone().0 + self.clone().request } pub fn headers(&self) -> &BTreeSet { - &self.1 + &self.headers } } impl Hash for DataLoaderRequest { fn hash(&self, state: &mut H) { - self.0.url().hash(state); + self.request.url().hash(state); // use body in hash for graphql queries with query operation as they used to // fetch data while http post and graphql mutation should not be loaded // through dataloader at all! - if let Some(body) = self.0.body() { + if let Some(body) = self.request.body() { body.as_bytes().hash(state); } - for name in &self.1 { - if let Some(value) = self.0.headers().get(name) { + for name in &self.headers { + if let Some(value) = self.request.headers().get(name) { name.hash(state); value.hash(state); } @@ -58,13 +72,15 @@ impl Eq for DataLoaderRequest {} impl Clone for DataLoaderRequest { fn clone(&self) -> Self { - let req = self.0.try_clone().unwrap_or_else(|| { - let mut req = reqwest::Request::new(self.0.method().clone(), self.0.url().clone()); - req.headers_mut().extend(self.0.headers().clone()); + let req = self.request.try_clone().unwrap_or_else(|| { + let mut req = + reqwest::Request::new(self.request.method().clone(), self.request.url().clone()); + req.headers_mut().extend(self.request.headers().clone()); req }); - DataLoaderRequest(req, self.1.clone()) + DataLoaderRequest::new(req, self.headers.clone()) + .with_batching_value(self.batching_value.clone()) } } @@ -72,7 +88,7 @@ impl Deref for DataLoaderRequest { type Target = reqwest::Request; fn deref(&self) -> &Self::Target { - &self.0 + &self.request } } diff --git a/src/core/http/mod.rs b/src/core/http/mod.rs index 499e47de6f..816f0fcb70 100644 --- a/src/core/http/mod.rs +++ b/src/core/http/mod.rs @@ -20,6 +20,7 @@ mod request_template; mod response; pub mod showcase; mod telemetry; +mod transformations; pub static TAILCALL_HTTPS_ORIGIN: HeaderValue = HeaderValue::from_static("https://tailcall.run"); pub static TAILCALL_HTTP_ORIGIN: HeaderValue = HeaderValue::from_static("http://tailcall.run"); diff --git a/src/core/http/request_template.rs b/src/core/http/request_template.rs index 748cd395a1..1c57f140db 100644 --- a/src/core/http/request_template.rs +++ b/src/core/http/request_template.rs @@ -12,6 +12,7 @@ use crate::core::endpoint::Endpoint; use crate::core::has_headers::HasHeaders; use crate::core::helpers::headers::MustacheHeaders; use crate::core::ir::model::{CacheKey, IoId}; +use crate::core::ir::DynamicRequest; use crate::core::mustache::{Eval, Mustache, Segment}; use crate::core::path::{PathString, PathValue, ValueString}; @@ -91,7 +92,7 @@ impl RequestTemplate { /// Returns true if there are not templates pub fn is_const(&self) -> bool { self.root_url.is_const() - && self.body_path.as_ref().map_or(true, Mustache::is_const) + && self.body_path.as_ref().map_or(true, |b| b.is_const()) && self.query.iter().all(|query| query.value.is_const()) && self.headers.iter().all(|(_, v)| v.is_const()) } @@ -113,15 +114,12 @@ impl RequestTemplate { pub fn to_request( &self, ctx: &C, - ) -> anyhow::Result { - // Create url + ) -> anyhow::Result> { let url = self.create_url(ctx)?; let method = self.method.clone(); - let mut req = reqwest::Request::new(method, url); - req = self.set_headers(req, ctx); - req = self.set_body(req, ctx)?; - - Ok(req) + let req = reqwest::Request::new(method, url); + let req = self.set_headers(req, ctx); + self.set_body(req, ctx) } /// Sets the body for the request @@ -129,26 +127,32 @@ impl RequestTemplate { &self, mut req: reqwest::Request, ctx: &C, - ) -> anyhow::Result { - if let Some(body_path) = &self.body_path { + ) -> anyhow::Result> { + let batching_value = if let Some(body_path) = &self.body_path { match &self.encoding { Encoding::ApplicationJson => { - req.body_mut().replace(body_path.render(ctx).into()); + let (body, batching_value) = + ExpressionValueEval::default().eval(body_path, ctx); + req.body_mut().replace(body.into()); + batching_value } Encoding::ApplicationXWwwFormUrlencoded => { // TODO: this is a performance bottleneck // We first encode everything to string and then back to form-urlencoded - let body: String = body_path.render(ctx); + let body = body_path.render(ctx); let form_data = match serde_json::from_str::(&body) { Ok(deserialized_data) => serde_urlencoded::to_string(deserialized_data)?, Err(_) => body, }; req.body_mut().replace(form_data.into()); + None } } - } - Ok(req) + } else { + None + }; + Ok(DynamicRequest::new(req).with_batching_value(batching_value)) } /// Sets the headers for the request @@ -231,7 +235,7 @@ impl TryFrom for RequestTemplate { let body = endpoint .body .as_ref() - .map(|body| Mustache::parse(body.as_str())); + .map(|b| Mustache::parse(&b.to_string())); let encoding = endpoint.encoding.clone(); Ok(Self { @@ -302,6 +306,50 @@ impl<'a, A: PathValue> Eval<'a> for ValueStringEval { } } +struct ExpressionValueEval(std::marker::PhantomData); +impl Default for ExpressionValueEval { + fn default() -> Self { + Self(std::marker::PhantomData) + } +} + +impl<'a, A: PathString> Eval<'a> for ExpressionValueEval { + type In = A; + type Out = (String, Option); + + fn eval(&self, mustache: &Mustache, in_value: &'a Self::In) -> Self::Out { + let mut result = String::new(); + // This evaluator returns a tuple of (evaluated_string, body_key) where: + // 1. evaluated_string: The fully rendered template string + // 2. body_key: The value of the first expression found in the template + // + // This implementation is a critical optimization for request batching: + // - During batching, we need to extract individual request values from the + // batch response and map them back to their original requests + // - Instead of parsing the body JSON multiple times, we extract the key during + // initial template evaluation + // - Since we enforce that batch requests can only contain one expression in + // their body, this key uniquely identifies each request + // - This approach eliminates the need for repeated JSON parsing/serialization + // during the batching process, significantly improving performance + let mut first_expression_value = None; + for segment in mustache.segments().iter() { + match segment { + Segment::Literal(text) => result.push_str(text), + Segment::Expression(parts) => { + if let Some(value) = in_value.path_string(parts) { + result.push_str(value.as_ref()); + if first_expression_value.is_none() { + first_expression_value = Some(value.into_owned()); + } + } + } + } + } + (result, first_expression_value) + } +} + #[cfg(test)] mod tests { use std::borrow::Cow; @@ -361,6 +409,7 @@ mod tests { ) -> anyhow::Result { let body = self .to_request(ctx)? + .into_request() .body() .and_then(|a| a.as_bytes()) .map(|a| a.to_vec()) @@ -398,8 +447,8 @@ mod tests { } })); - let req = tmpl.to_request(&ctx).unwrap(); - + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!( req.url().to_string(), "http://localhost:3000/?baz=1&baz=2&baz=3&foo=12" @@ -410,7 +459,8 @@ mod tests { fn test_url() { let tmpl = RequestTemplate::new("http://localhost:3000/").unwrap(); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.url().to_string(), "http://localhost:3000/"); } @@ -418,7 +468,8 @@ mod tests { fn test_url_path() { let tmpl = RequestTemplate::new("http://localhost:3000/foo/bar").unwrap(); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.url().to_string(), "http://localhost:3000/foo/bar"); } @@ -431,7 +482,8 @@ mod tests { } })); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.url().to_string(), "http://localhost:3000/foo/bar"); } @@ -446,7 +498,9 @@ mod tests { "booz": 1 } })); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); + assert_eq!( req.url().to_string(), "http://localhost:3000/foo/bar/boozes/1" @@ -472,11 +526,15 @@ mod tests { skip_empty: false, }, ]; + let tmpl = RequestTemplate::new("http://localhost:3000") .unwrap() .query(query); + let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); + assert_eq!( req.url().to_string(), "http://localhost:3000/?foo=0&bar=1&baz=2" @@ -513,7 +571,8 @@ mod tests { "id": 2 } })); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!( req.url().to_string(), "http://localhost:3000/?foo=0&bar=1&baz=2" @@ -531,7 +590,8 @@ mod tests { .unwrap() .headers(headers); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.headers().get("foo").unwrap(), "foo"); assert_eq!(req.headers().get("bar").unwrap(), "bar"); assert_eq!(req.headers().get("baz").unwrap(), "baz"); @@ -561,7 +621,8 @@ mod tests { "id": 2 } })); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.headers().get("foo").unwrap(), "0"); assert_eq!(req.headers().get("bar").unwrap(), "1"); assert_eq!(req.headers().get("baz").unwrap(), "2"); @@ -574,7 +635,8 @@ mod tests { .method(reqwest::Method::POST) .encoding(crate::core::config::Encoding::ApplicationJson); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!( req.headers().get("Content-Type").unwrap(), "application/json" @@ -588,7 +650,8 @@ mod tests { .method(reqwest::Method::POST) .encoding(crate::core::config::Encoding::ApplicationXWwwFormUrlencoded); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!( req.headers().get("Content-Type").unwrap(), "application/x-www-form-urlencoded" @@ -601,7 +664,8 @@ mod tests { .unwrap() .method(reqwest::Method::POST); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.method(), reqwest::Method::POST); } @@ -662,11 +726,12 @@ mod tests { .body(Some("foo".into())); let tmpl = RequestTemplate::try_from(endpoint).unwrap(); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let req_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = req_wrapper.request(); assert_eq!(req.method(), reqwest::Method::POST); assert_eq!(req.headers().get("foo").unwrap(), "bar"); let body = req.body().unwrap().as_bytes().unwrap().to_owned(); - assert_eq!(body, "foo".as_bytes()); + assert_eq!(body, "\"foo\"".as_bytes()); assert_eq!(req.url().to_string(), "http://localhost:3000/"); } @@ -688,7 +753,8 @@ mod tests { "header": "abc" } })); - let req = tmpl.to_request(&ctx).unwrap(); + let req_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = req_wrapper.request(); assert_eq!(req.method(), reqwest::Method::POST); assert_eq!(req.headers().get("foo").unwrap(), "abc"); let body = req.body().unwrap().as_bytes().unwrap().to_owned(); @@ -703,7 +769,8 @@ mod tests { ); let tmpl = RequestTemplate::try_from(endpoint).unwrap(); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.url().to_string(), "http://localhost:3000/"); } @@ -718,7 +785,8 @@ mod tests { ]); let tmpl = RequestTemplate::try_from(endpoint).unwrap(); let ctx = Context::default(); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.url().to_string(), "http://localhost:3000/?q=1&b=1&c"); } @@ -734,7 +802,8 @@ mod tests { "d": "bar" } })); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!( req.url().to_string(), "http://localhost:3000/foo?b=foo&d=bar" @@ -758,7 +827,8 @@ mod tests { "d": "bar", } })); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!( req.url().to_string(), "http://localhost:3000/foo?b=foo&d=bar&f=baz&e" @@ -773,7 +843,8 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("baz", "qux".parse().unwrap()); let ctx = Context::default().headers(headers); - let req = tmpl.to_request(&ctx).unwrap(); + let request_wrapper = tmpl.to_request(&ctx).unwrap(); + let req = request_wrapper.request(); assert_eq!(req.headers().get("baz").unwrap(), "qux"); } } @@ -790,7 +861,8 @@ mod tests { let tmpl = RequestTemplate::form_encoded_url("http://localhost:3000") .unwrap() .body_path(Some(Mustache::parse("{{foo.bar}}"))); - let ctx = Context::default().value(json!({"foo": {"bar": "baz"}})); + let ctx = Context::default().value(json!({"foo": {"bar": + "baz"}})); let request_body = tmpl.to_body(&ctx); let body = request_body.unwrap(); assert_eq!(body, "baz"); diff --git a/src/core/http/transformations/body_batching.rs b/src/core/http/transformations/body_batching.rs new file mode 100644 index 0000000000..173b776546 --- /dev/null +++ b/src/core/http/transformations/body_batching.rs @@ -0,0 +1,248 @@ +use std::convert::Infallible; + +use reqwest::Request; +use tailcall_valid::Valid; + +use crate::core::http::DataLoaderRequest; +use crate::core::Transform; + +pub struct BodyBatching<'a> { + dl_requests: &'a [&'a DataLoaderRequest], +} + +impl<'a> BodyBatching<'a> { + pub fn new(dl_requests: &'a [&'a DataLoaderRequest]) -> Self { + BodyBatching { dl_requests } + } +} + +impl Transform for BodyBatching<'_> { + type Value = Request; + type Error = Infallible; + + // This function is used to batch the body of the requests. + // working of this function is as follows: + // 1. It takes the list of requests and extracts the body from each request. + // 2. It then clubs all the extracted bodies into list format. like [body1, + // body2, body3] + // 3. It does this all manually to avoid extra serialization cost. + fn transform(&self, mut base_request: Self::Value) -> Valid { + let mut request_bodies = Vec::with_capacity(self.dl_requests.len()); + + for req in self.dl_requests { + if let Some(body) = req.body().and_then(|b| b.as_bytes()) { + request_bodies.push(body); + } + } + + if !request_bodies.is_empty() { + if cfg!(debug_assertions) { + // sort the body to make it consistent for testing env. + request_bodies.sort(); + } + + // construct serialization manually. + let merged_body = request_bodies.iter().fold( + Vec::with_capacity( + request_bodies.iter().map(|i| i.len()).sum::() + request_bodies.len(), + ), + |mut acc, item| { + if !acc.is_empty() { + // add ',' to separate the body from each other. + acc.extend_from_slice(b","); + } + acc.extend_from_slice(item); + acc + }, + ); + + // add list brackets to the serialized body. + let mut serialized_body = Vec::with_capacity(merged_body.len() + 2); + serialized_body.extend_from_slice(b"["); + serialized_body.extend_from_slice(&merged_body); + serialized_body.extend_from_slice(b"]"); + base_request.body_mut().replace(serialized_body.into()); + } + + Valid::succeed(base_request) + } +} + +#[cfg(test)] +mod tests { + use http::Method; + use reqwest::Request; + use serde_json::json; + use tailcall_valid::Validator; + + use super::*; + use crate::core::http::DataLoaderRequest; + + fn create_request(body: Option) -> DataLoaderRequest { + let mut request = create_base_request(); + if let Some(body) = body { + let bytes_body = serde_json::to_vec(&body).unwrap(); + request.body_mut().replace(reqwest::Body::from(bytes_body)); + } + + DataLoaderRequest::new(request, Default::default()) + } + + fn create_base_request() -> Request { + Request::new(Method::POST, "http://example.com".parse().unwrap()) + } + + #[test] + fn test_empty_requests() { + let requests: Vec<&DataLoaderRequest> = vec![]; + let base_request = create_base_request(); + + let result = BodyBatching::new(&requests) + .transform(base_request) + .to_result() + .unwrap(); + + assert!(result.body().is_none()); + } + + #[test] + fn test_single_request() { + let req = create_request(Some(json!({"id": 1}))); + let requests = vec![&req]; + let base_request = create_base_request(); + + let request = BodyBatching::new(&requests) + .transform(base_request) + .to_result() + .unwrap(); + + let bytes = request + .body() + .and_then(|b| b.as_bytes()) + .unwrap_or_default(); + let body_str = String::from_utf8(bytes.to_vec()).unwrap(); + assert_eq!(body_str, r#"[{"id":1}]"#); + } + + #[test] + fn test_multiple_requests() { + let req1 = create_request(Some(json!({"id": 1}))); + let req2 = create_request(Some(json!({"id": 2}))); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = BodyBatching::new(&requests) + .transform(base_request) + .to_result() + .unwrap(); + + let body = result.body().and_then(|b| b.as_bytes()).unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, r#"[{"id":1},{"id":2}]"#); + } + + #[test] + fn test_requests_with_empty_bodies() { + let req1 = create_request(Some(json!({"id": 1}))); + let req2 = create_request(None); + let req3 = create_request(Some(json!({"id": 3}))); + let requests = vec![&req1, &req2, &req3]; + let base_request = create_base_request(); + + let result = BodyBatching::new(&requests) + .transform(base_request) + .to_result() + .unwrap(); + + let body_bytes = result + .body() + .and_then(|b| b.as_bytes()) + .expect("Body should be present"); + let parsed: Vec = serde_json::from_slice(body_bytes).unwrap(); + + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0]["id"], 1); + assert_eq!(parsed[1]["id"], 3); + } + + #[test] + #[cfg(test)] + fn test_body_sorting_in_test_env() { + let req1 = create_request(Some(json!({ + "id": 2, + "value": "second" + }))); + let req2 = create_request(Some(json!({ + "id": 1, + "value": "first" + }))); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = BodyBatching::new(&requests) + .transform(base_request) + .to_result() + .unwrap(); + + let body_bytes = result + .body() + .and_then(|b| b.as_bytes()) + .expect("Body should be present"); + let parsed: Vec = serde_json::from_slice(body_bytes).unwrap(); + + // Verify sorting by comparing the serialized form + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0]["id"], 1); + assert_eq!(parsed[0]["value"], "first"); + assert_eq!(parsed[1]["id"], 2); + assert_eq!(parsed[1]["value"], "second"); + } + + #[test] + fn test_complex_json_bodies() { + let req1 = create_request(Some(json!({ + "id": 1, + "nested": { + "array": [1, 2, 3], + "object": {"key": "value"} + }, + "tags": ["a", "b", "c"] + }))); + let req2 = create_request(Some(json!({ + "id": 2, + "nested": { + "array": [4, 5, 6], + "object": {"key": "another"} + }, + "tags": ["x", "y", "z"] + }))); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = BodyBatching::new(&requests) + .transform(base_request) + .to_result() + .unwrap(); + + let body_bytes = result + .body() + .and_then(|b| b.as_bytes()) + .expect("Body should be present"); + let parsed: Vec = serde_json::from_slice(body_bytes).unwrap(); + + // Verify structure and content of both objects + assert_eq!(parsed.len(), 2); + + // First object + assert_eq!(parsed[0]["id"], 1); + assert_eq!(parsed[0]["nested"]["array"], json!([1, 2, 3])); + assert_eq!(parsed[0]["nested"]["object"]["key"], "value"); + assert_eq!(parsed[0]["tags"], json!(["a", "b", "c"])); + + // Second object + assert_eq!(parsed[1]["id"], 2); + assert_eq!(parsed[1]["nested"]["array"], json!([4, 5, 6])); + assert_eq!(parsed[1]["nested"]["object"]["key"], "another"); + assert_eq!(parsed[1]["tags"], json!(["x", "y", "z"])); + } +} diff --git a/src/core/http/transformations/mod.rs b/src/core/http/transformations/mod.rs new file mode 100644 index 0000000000..b6ab71810c --- /dev/null +++ b/src/core/http/transformations/mod.rs @@ -0,0 +1,5 @@ +mod body_batching; +mod query_batching; + +pub use body_batching::BodyBatching; +pub use query_batching::QueryBatching; diff --git a/src/core/http/transformations/query_batching.rs b/src/core/http/transformations/query_batching.rs new file mode 100644 index 0000000000..1612608388 --- /dev/null +++ b/src/core/http/transformations/query_batching.rs @@ -0,0 +1,200 @@ +use std::convert::Infallible; + +use reqwest::Request; +use tailcall_valid::Valid; + +use crate::core::http::DataLoaderRequest; +use crate::core::Transform; + +pub struct QueryBatching<'a> { + dl_requests: &'a [&'a DataLoaderRequest], + group_by: Option<&'a str>, +} + +impl<'a> QueryBatching<'a> { + pub fn new(dl_requests: &'a [&'a DataLoaderRequest], group_by: Option<&'a str>) -> Self { + QueryBatching { dl_requests, group_by } + } +} + +impl Transform for QueryBatching<'_> { + type Value = Request; + type Error = Infallible; + fn transform(&self, mut base_request: Self::Value) -> Valid { + // Merge query params in the request + for key in self.dl_requests.iter() { + let request = key.to_request(); + let url = request.url(); + let pairs: Vec<_> = if let Some(group_by_key) = self.group_by { + url.query_pairs() + .filter(|(key, _)| group_by_key.eq(&key.to_string())) + .collect() + } else { + url.query_pairs().collect() + }; + + if !pairs.is_empty() { + // if pair's are empty then don't extend the query params else it ends + // up appending '?' to the url. + base_request.url_mut().query_pairs_mut().extend_pairs(pairs); + } + } + Valid::succeed(base_request) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use http::Method; + use reqwest::Url; + use tailcall_valid::Validator; + + use super::*; + + fn create_base_request() -> Request { + Request::new(Method::GET, "http://example.com".parse().unwrap()) + } + + fn create_request_with_params(params: &[(&str, &str)]) -> DataLoaderRequest { + let mut url = Url::parse("http://example.com").unwrap(); + { + let mut query_pairs = url.query_pairs_mut(); + for (key, value) in params { + query_pairs.append_pair(key, value); + } + } + let request = Request::new(Method::GET, url); + DataLoaderRequest::new(request, Default::default()) + } + + fn get_query_params(request: &Request) -> HashMap { + request + .url() + .query_pairs() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } + + #[test] + fn test_empty_requests() { + let requests: Vec<&DataLoaderRequest> = vec![]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, None) + .transform(base_request) + .to_result() + .unwrap(); + + assert!(result.url().query().is_none()); + } + + #[test] + fn test_single_request_no_grouping() { + let req = create_request_with_params(&[("id", "1"), ("name", "test")]); + let requests = vec![&req]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, None) + .transform(base_request) + .to_result() + .unwrap(); + + let params = get_query_params(&result); + assert_eq!(params.len(), 2); + assert_eq!(params.get("id").unwrap(), "1"); + assert_eq!(params.get("name").unwrap(), "test"); + } + + #[test] + fn test_multiple_requests_with_grouping() { + let req1 = create_request_with_params(&[("user_id", "1"), ("extra", "data1")]); + let req2 = create_request_with_params(&[("user_id", "2"), ("extra", "data2")]); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, Some("user_id")) + .transform(base_request) + .to_result() + .unwrap(); + + let params = get_query_params(&result); + assert!(params.contains_key("user_id")); + assert!(!params.contains_key("extra")); + + // URL should contain both user_ids + let url = result.url().to_string(); + assert!(url.contains("user_id=1")); + assert!(url.contains("user_id=2")); + } + + #[test] + fn test_multiple_requests_no_grouping() { + let req1 = create_request_with_params(&[("param1", "value1"), ("shared", "a")]); + let req2 = create_request_with_params(&[("param2", "value2"), ("shared", "b")]); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, None) + .transform(base_request) + .to_result() + .unwrap(); + + let params = get_query_params(&result); + assert_eq!(params.get("param1").unwrap(), "value1"); + assert_eq!(params.get("param2").unwrap(), "value2"); + assert_eq!(params.get("shared").unwrap(), "b"); + } + + #[test] + fn test_requests_with_empty_params() { + let req1 = create_request_with_params(&[("id", "1")]); + let req2 = create_request_with_params(&[]); + let req3 = create_request_with_params(&[("id", "3")]); + let requests = vec![&req1, &req2, &req3]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, Some("id")) + .transform(base_request) + .to_result() + .unwrap(); + + let url = result.url().to_string(); + assert!(url.contains("id=1")); + assert!(url.contains("id=3")); + } + + #[test] + fn test_special_characters() { + let req1 = create_request_with_params(&[("query", "hello world"), ("tag", "a+b")]); + let req2 = create_request_with_params(&[("query", "foo&bar"), ("tag", "c%20d")]); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, None) + .transform(base_request) + .to_result() + .unwrap(); + + let params = get_query_params(&result); + // Verify URL encoding is preserved + assert!(params.values().any(|v| v.contains(" ") || v.contains("&"))); + } + + #[test] + fn test_group_by_with_missing_key() { + let req1 = create_request_with_params(&[("id", "1"), ("data", "test")]); + let req2 = create_request_with_params(&[("other", "2"), ("data", "test2")]); + let requests = vec![&req1, &req2]; + let base_request = create_base_request(); + + let result = QueryBatching::new(&requests, Some("missing_key")) + .transform(base_request) + .to_result() + .unwrap(); + + // Should have no query parameters since grouped key doesn't exist + assert!(result.url().query().is_none()); + } +} diff --git a/src/core/ir/eval_http.rs b/src/core/ir/eval_http.rs index f196b7d017..d863472fbd 100644 --- a/src/core/ir/eval_http.rs +++ b/src/core/ir/eval_http.rs @@ -5,6 +5,7 @@ use reqwest::Request; use tailcall_valid::Validator; use super::model::DataLoaderId; +use super::request::DynamicRequest; use super::{EvalContext, ResolverContextLike}; use crate::core::data_loader::{DataLoader, Loader}; use crate::core::grpc::protobuf::ProtobufOperation; @@ -68,15 +69,18 @@ impl<'a, 'ctx, Context: ResolverContextLike + Sync> EvalHttp<'a, 'ctx, Context> Self { evaluation_ctx, data_loader, request_template } } - pub fn init_request(&self) -> Result { - Ok(self.request_template.to_request(self.evaluation_ctx)?) + pub fn init_request(&self) -> Result, Error> { + let inner = self.request_template.to_request(self.evaluation_ctx)?; + Ok(inner) } - pub async fn execute(&self, req: Request) -> Result, Error> { + pub async fn execute( + &self, + req: DynamicRequest, + ) -> Result, Error> { let ctx = &self.evaluation_ctx; - let is_get = req.method() == reqwest::Method::GET; let dl = &self.data_loader; - let response = if is_get && dl.is_some() { + let response = if dl.is_some() { execute_request_with_dl(ctx, req, self.data_loader).await? } else { execute_raw_request(ctx, req).await? @@ -99,7 +103,7 @@ impl<'a, 'ctx, Context: ResolverContextLike + Sync> EvalHttp<'a, 'ctx, Context> #[async_recursion::async_recursion] pub async fn execute_with_worker<'worker: 'async_recursion>( &self, - mut request: reqwest::Request, + mut request: DynamicRequest, worker_ctx: WorkerContext<'worker>, ) -> Result, Error> { // extract variables from the worker context. @@ -107,10 +111,10 @@ impl<'a, 'ctx, Context: ResolverContextLike + Sync> EvalHttp<'a, 'ctx, Context> let worker = worker_ctx.worker; let js_worker = worker_ctx.js_worker; - let response = match js_hooks.on_request(worker, &request).await? { + let response = match js_hooks.on_request(worker, request.request()).await? { Some(command) => match command { worker::Command::Request(w_request) => { - let response = self.execute(w_request.into()).await?; + let response = self.execute(w_request.try_into()?).await?; Ok(response) } worker::Command::Response(w_response) => { @@ -119,6 +123,7 @@ impl<'a, 'ctx, Context: ResolverContextLike + Sync> EvalHttp<'a, 'ctx, Context> && w_response.headers().contains_key("location") { request + .request_mut() .url_mut() .set_path(w_response.headers()["location"].as_str()); self.execute_with_worker(request, worker_ctx).await @@ -145,7 +150,7 @@ pub async fn execute_request_with_dl< Dl: Loader, Error = Arc>, >( ctx: &EvalContext<'ctx, Ctx>, - req: Request, + req: DynamicRequest, data_loader: Option<&DataLoader>, ) -> Result, Error> { let headers = ctx @@ -155,7 +160,10 @@ pub async fn execute_request_with_dl< .clone() .map(|s| s.headers) .unwrap_or_default(); - let endpoint_key = crate::core::http::DataLoaderRequest::new(req, headers); + + let (req, batching_value) = req.into_parts(); + let endpoint_key = + crate::core::http::DataLoaderRequest::new(req, headers).with_batching_value(batching_value); Ok(data_loader .unwrap() @@ -203,13 +211,13 @@ fn set_cookie_headers( pub async fn execute_raw_request( ctx: &EvalContext<'_, Ctx>, - req: Request, + req: DynamicRequest, ) -> Result, Error> { let response = ctx .request_ctx .runtime .http - .execute(req) + .execute(req.into_request()) .await .map_err(Error::from)? .to_json()?; diff --git a/src/core/ir/eval_io.rs b/src/core/ir/eval_io.rs index cc8f27e7c3..f9ef59b3da 100644 --- a/src/core/ir/eval_io.rs +++ b/src/core/ir/eval_io.rs @@ -5,7 +5,7 @@ use super::eval_http::{ execute_request_with_dl, parse_graphql_response, set_headers, EvalHttp, WorkerContext, }; use super::model::{CacheKey, IO}; -use super::{EvalContext, ResolverContextLike}; +use super::{DynamicRequest, EvalContext, ResolverContextLike}; use crate::core::config::GraphQLOperationType; use crate::core::data_loader::DataLoader; use crate::core::graphql::GraphqlDataLoader; @@ -62,15 +62,15 @@ where } IO::GraphQL { req_template, field_name, dl_id, .. } => { let req = req_template.to_request(ctx)?; - + let request = DynamicRequest::new(req); let res = if ctx.request_ctx.upstream.batch.is_some() && matches!(req_template.operation_type, GraphQLOperationType::Query) { let data_loader: Option<&DataLoader> = dl_id.and_then(|dl| ctx.request_ctx.gql_data_loaders.get(dl.as_usize())); - execute_request_with_dl(ctx, req, data_loader).await? + execute_request_with_dl(ctx, request, data_loader).await? } else { - execute_raw_request(ctx, req).await? + execute_raw_request(ctx, request).await? }; set_headers(ctx, &res); diff --git a/src/core/ir/mod.rs b/src/core/ir/mod.rs index 4540424848..99008c585b 100644 --- a/src/core/ir/mod.rs +++ b/src/core/ir/mod.rs @@ -4,6 +4,7 @@ mod eval; mod eval_context; mod eval_http; mod eval_io; +mod request; mod resolver_context_like; pub mod model; @@ -13,6 +14,7 @@ use std::ops::Deref; pub use discriminator::*; pub use error::*; pub use eval_context::EvalContext; +pub(crate) use request::DynamicRequest; pub use resolver_context_like::{ EmptyResolverContext, ResolverContext, ResolverContextLike, SelectionField, }; diff --git a/src/core/ir/request.rs b/src/core/ir/request.rs new file mode 100644 index 0000000000..7d5334f360 --- /dev/null +++ b/src/core/ir/request.rs @@ -0,0 +1,40 @@ +/// Holds necessary information for request execution. +pub struct DynamicRequest { + request: reqwest::Request, + /// used for request body batching. + batching_value: Option, +} + +impl DynamicRequest { + pub fn new(request: reqwest::Request) -> Self { + Self { request, batching_value: None } + } + + pub fn with_batching_value(self, body_key: Option) -> Self { + Self { batching_value: body_key, ..self } + } + + pub fn request(&self) -> &reqwest::Request { + &self.request + } + + pub fn request_mut(&mut self) -> &mut reqwest::Request { + &mut self.request + } + + pub fn body_key(&self) -> Option<&Value> { + self.batching_value.as_ref() + } + + pub fn into_request(self) -> reqwest::Request { + self.request + } + + pub fn into_body_key(self) -> Option { + self.batching_value + } + + pub fn into_parts(self) -> (reqwest::Request, Option) { + (self.request, self.batching_value) + } +} diff --git a/src/core/json/borrow.rs b/src/core/json/borrow.rs index ca93926b4f..60edafcc77 100644 --- a/src/core/json/borrow.rs +++ b/src/core/json/borrow.rs @@ -35,6 +35,14 @@ impl<'ctx> JsonObjectLike<'ctx> for ObjectAsVec<'ctx> { { self.iter() } + + fn len(&self) -> usize { + self.len() + } + + fn is_empty(&self) -> bool { + self.is_empty() + } } impl<'ctx> JsonLike<'ctx> for Value<'ctx> { diff --git a/src/core/json/graphql.rs b/src/core/json/graphql.rs index cb44568d8b..5491625461 100644 --- a/src/core/json/graphql.rs +++ b/src/core/json/graphql.rs @@ -37,6 +37,14 @@ impl<'obj, Value: JsonLike<'obj>> JsonObjectLike<'obj> for IndexMap { self.iter().map(|(k, v)| (k.as_str(), v)) } + + fn len(&self) -> usize { + self.len() + } + + fn is_empty(&self) -> bool { + self.is_empty() + } } impl<'json> JsonLike<'json> for ConstValue { diff --git a/src/core/json/json_like.rs b/src/core/json/json_like.rs index 3a346b576f..364bee32d6 100644 --- a/src/core/json/json_like.rs +++ b/src/core/json/json_like.rs @@ -27,7 +27,7 @@ pub trait JsonLike<'json>: Sized { T::JsonObject: JsonObjectLike<'json, Value = T>, { if let Some(obj) = other.as_object() { - let mut fields = Vec::new(); + let mut fields = Vec::with_capacity(obj.len()); for (k, v) in obj.iter() { fields.push((k, Self::clone_from(v))); } @@ -67,6 +67,8 @@ pub trait JsonLike<'json>: Sized { pub trait JsonObjectLike<'obj>: Sized { type Value; fn new() -> Self; + fn is_empty(&self) -> bool; + fn len(&self) -> usize; fn with_capacity(n: usize) -> Self; fn from_vec(v: Vec<(&'obj str, Self::Value)>) -> Self; fn get_key(&self, key: &str) -> Option<&Self::Value>; diff --git a/src/core/json/serde.rs b/src/core/json/serde.rs index 62beed0f45..b726b061a6 100644 --- a/src/core/json/serde.rs +++ b/src/core/json/serde.rs @@ -35,6 +35,14 @@ impl<'obj> JsonObjectLike<'obj> for serde_json::Map { { self.iter().map(|(k, v)| (k.as_str(), v)) } + + fn len(&self) -> usize { + self.len() + } + + fn is_empty(&self) -> bool { + self.is_empty() + } } impl<'json> JsonLike<'json> for Value { diff --git a/src/core/worker/worker.rs b/src/core/worker/worker.rs index 4e6ef46dde..9b9ddd124e 100644 --- a/src/core/worker/worker.rs +++ b/src/core/worker/worker.rs @@ -3,9 +3,11 @@ use std::fmt::Display; use hyper::body::Bytes; use reqwest::Request; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use super::error::{Error, Result}; +use crate::core::ir::DynamicRequest; use crate::core::{is_default, Response}; #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)] @@ -186,6 +188,14 @@ impl From for reqwest::Request { } } +impl TryFrom for DynamicRequest { + type Error = anyhow::Error; + + fn try_from(value: WorkerRequest) -> std::result::Result { + Ok(DynamicRequest::new(value.0)) + } +} + impl From<&reqwest::Url> for Uri { fn from(value: &reqwest::Url) -> Self { Self { diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap index 23a4aba11f..b4581cc432 100644 --- a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap @@ -60,12 +60,12 @@ type News { } type Query { - GEN__news__NewsService__AddNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/AddNews", body: "\"{{.args.news}}\"", method: "POST") - GEN__news__NewsService__DeleteNews(newsId: Id!): Empty @http(url: "http://localhost:50051/news.NewsService/DeleteNews", body: "\"{{.args.newsId}}\"", method: "POST") - GEN__news__NewsService__EditNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/EditNews", body: "\"{{.args.news}}\"", method: "POST") - GEN__news__NewsService__GetAllNews: GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetAllNews", body: "{}", method: "POST") - GEN__news__NewsService__GetMultipleNews(multipleNewsId: GEN__news__MultipleNewsId!): GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetMultipleNews", body: "\"{{.args.multipleNewsId}}\"", method: "POST") - GEN__news__NewsService__GetNews(newsId: Id!): News @http(url: "http://localhost:50051/news.NewsService/GetNews", body: "\"{{.args.newsId}}\"", method: "POST") + GEN__news__NewsService__AddNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/AddNews", body: "{{.args.news}}", method: "POST") + GEN__news__NewsService__DeleteNews(newsId: Id!): Empty @http(url: "http://localhost:50051/news.NewsService/DeleteNews", body: "{{.args.newsId}}", method: "POST") + GEN__news__NewsService__EditNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/EditNews", body: "{{.args.news}}", method: "POST") + GEN__news__NewsService__GetAllNews: GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetAllNews", body: {}, method: "POST") + GEN__news__NewsService__GetMultipleNews(multipleNewsId: GEN__news__MultipleNewsId!): GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetMultipleNews", body: "{{.args.multipleNewsId}}", method: "POST") + GEN__news__NewsService__GetNews(newsId: Id!): News @http(url: "http://localhost:50051/news.NewsService/GetNews", body: "{{.args.newsId}}", method: "POST") users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") } diff --git a/tests/core/snapshots/batching-validation.md_error.snap b/tests/core/snapshots/batching-validation.md_error.snap new file mode 100644 index 0000000000..7c03000416 --- /dev/null +++ b/tests/core/snapshots/batching-validation.md_error.snap @@ -0,0 +1,45 @@ +--- +source: tests/core/spec.rs +expression: errors +--- +[ + { + "message": "batchKey requires either body or query parameters", + "trace": [ + "Query", + "posts", + "@http" + ], + "description": null + }, + { + "message": "Request body batching requires exactly one dynamic value in the body.", + "trace": [ + "Query", + "user", + "@http", + "body" + ], + "description": null + }, + { + "message": "Request body batching requires exactly one dynamic value in the body.", + "trace": [ + "Query", + "userWithId", + "@http", + "body" + ], + "description": null + }, + { + "message": "Request body batching requires exactly one dynamic value in the body.", + "trace": [ + "Query", + "userWithIdTest", + "@http", + "body" + ], + "description": null + } +] diff --git a/tests/core/snapshots/body-batching-cases.md_0.snap b/tests/core/snapshots/body-batching-cases.md_0.snap new file mode 100644 index 0000000000..cd99803cc2 --- /dev/null +++ b/tests/core/snapshots/body-batching-cases.md_0.snap @@ -0,0 +1,34 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "users": [ + { + "id": 1, + "name": "user-1", + "post": { + "id": 1, + "title": "user-1", + "userId": 1 + } + }, + { + "id": 2, + "name": "user-2", + "post": { + "id": 2, + "title": "user-2", + "userId": 2 + } + } + ] + } + } +} diff --git a/tests/core/snapshots/body-batching-cases.md_1.snap b/tests/core/snapshots/body-batching-cases.md_1.snap new file mode 100644 index 0000000000..c555390e10 --- /dev/null +++ b/tests/core/snapshots/body-batching-cases.md_1.snap @@ -0,0 +1,32 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "posts": [ + { + "id": 1, + "title": "user-1", + "user": { + "id": 1, + "name": "user-1" + } + }, + { + "id": 2, + "title": "user-2", + "user": { + "id": 2, + "name": "user-2" + } + } + ] + } + } +} diff --git a/tests/core/snapshots/body-batching-cases.md_2.snap b/tests/core/snapshots/body-batching-cases.md_2.snap new file mode 100644 index 0000000000..3957e2c1a4 --- /dev/null +++ b/tests/core/snapshots/body-batching-cases.md_2.snap @@ -0,0 +1,32 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "foo": [ + { + "a": 11, + "b": 12, + "bar": { + "a": 11, + "b": 12 + } + }, + { + "a": 21, + "b": 22, + "bar": { + "a": 21, + "b": 22 + } + } + ] + } + } +} diff --git a/tests/core/snapshots/body-batching-cases.md_client.snap b/tests/core/snapshots/body-batching-cases.md_client.snap new file mode 100644 index 0000000000..2c8ce93782 --- /dev/null +++ b/tests/core/snapshots/body-batching-cases.md_client.snap @@ -0,0 +1,40 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +type Bar { + a: Int + b: Int +} + +type Foo { + a: Int + b: Int + bar: Bar +} + +type Post { + body: String! + id: Int! + title: String! + user: User + userId: Int! +} + +type Query { + foo: [Foo] + posts: [Post] + user: User + users: [User] +} + +type User { + email: String! + id: Int! + name: String! + post: Post +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/body-batching-cases.md_merged.snap b/tests/core/snapshots/body-batching-cases.md_merged.snap new file mode 100644 index 0000000000..a546176af5 --- /dev/null +++ b/tests/core/snapshots/body-batching-cases.md_merged.snap @@ -0,0 +1,53 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server(port: 8000) @upstream(batch: {delay: 1, headers: []}, httpCache: 42) { + query: Query +} + +type Bar { + a: Int + b: Int +} + +type Foo { + a: Int + b: Int + bar: Bar + @http(url: "http://jsonplaceholder.typicode.com/bar", body: {id: "{{.value.a}}"}, batchKey: ["a"], method: "POST") +} + +type Post { + body: String! + id: Int! + title: String! + user: User + @http( + url: "http://jsonplaceholder.typicode.com/users" + body: {key: "id", value: "{{.value.userId}}"} + batchKey: ["id"] + method: "POST" + ) + userId: Int! +} + +type Query { + foo: [Foo] @http(url: "http://jsonplaceholder.typicode.com/foo") + posts: [Post] @http(url: "http://jsonplaceholder.typicode.com/posts") + user: User @http(url: "http://jsonplaceholder.typicode.com/users/1") + users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") +} + +type User { + email: String! + id: Int! + name: String! + post: Post + @http( + url: "http://jsonplaceholder.typicode.com/posts" + body: {userId: "{{.value.id}}", title: "title", body: "body"} + batchKey: ["userId"] + method: "POST" + ) +} diff --git a/tests/core/snapshots/body-batching.md_0.snap b/tests/core/snapshots/body-batching.md_0.snap new file mode 100644 index 0000000000..ab8ac6b554 --- /dev/null +++ b/tests/core/snapshots/body-batching.md_0.snap @@ -0,0 +1,43 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "users": [ + { + "id": 1, + "posts": [ + { + "userId": 1, + "title": "foo" + } + ] + }, + { + "id": 2, + "posts": [ + { + "userId": 2, + "title": "foo" + } + ] + }, + { + "id": 3, + "posts": [ + { + "userId": 3, + "title": "foo" + } + ] + } + ] + } + } +} diff --git a/tests/core/snapshots/body-batching.md_1.snap b/tests/core/snapshots/body-batching.md_1.snap new file mode 100644 index 0000000000..ec1863ee43 --- /dev/null +++ b/tests/core/snapshots/body-batching.md_1.snap @@ -0,0 +1,40 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "users": [ + { + "id": 1, + "comments": [ + { + "id": 1 + } + ] + }, + { + "id": 2, + "comments": [ + { + "id": 2 + } + ] + }, + { + "id": 3, + "comments": [ + { + "id": 3 + } + ] + } + ] + } + } +} diff --git a/tests/core/snapshots/body-batching.md_client.snap b/tests/core/snapshots/body-batching.md_client.snap new file mode 100644 index 0000000000..92f94eba7f --- /dev/null +++ b/tests/core/snapshots/body-batching.md_client.snap @@ -0,0 +1,29 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +type Comment { + id: Int +} + +type Post { + body: String + id: Int + title: String + userId: Int! +} + +type Query { + users: [User] +} + +type User { + comments: [Comment] + id: Int! + name: String! + posts: [Post] +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/body-batching.md_merged.snap b/tests/core/snapshots/body-batching.md_merged.snap new file mode 100644 index 0000000000..3c4b7a45b6 --- /dev/null +++ b/tests/core/snapshots/body-batching.md_merged.snap @@ -0,0 +1,43 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema + @server(port: 8000, queryValidation: false) + @upstream(batch: {delay: 1, headers: [], maxSize: 1000}, httpCache: 42) { + query: Query +} + +type Comment { + id: Int +} + +type Post { + body: String + id: Int + title: String + userId: Int! +} + +type Query { + users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") +} + +type User { + comments: [Comment] + @http( + url: "https://jsonplaceholder.typicode.com/comments" + body: {title: "foo", body: "bar", meta: {information: {userId: "{{.value.id}}"}}} + batchKey: ["userId"] + method: "POST" + ) + id: Int! + name: String! + posts: [Post] + @http( + url: "https://jsonplaceholder.typicode.com/posts" + body: {userId: "{{.value.id}}", title: "foo", body: "bar"} + batchKey: ["userId"] + method: "POST" + ) +} diff --git a/tests/core/snapshots/test-batch-operator-post.md_error.snap b/tests/core/snapshots/test-batch-operator-post.md_error.snap index 3ac6e3e721..d7898cfbcf 100644 --- a/tests/core/snapshots/test-batch-operator-post.md_error.snap +++ b/tests/core/snapshots/test-batch-operator-post.md_error.snap @@ -4,7 +4,7 @@ expression: errors --- [ { - "message": "GroupBy is only supported for GET requests", + "message": "batchKey requires either body or query parameters", "trace": [ "Query", "user", diff --git a/tests/execution/batching-validation.md b/tests/execution/batching-validation.md new file mode 100644 index 0000000000..208107efc9 --- /dev/null +++ b/tests/execution/batching-validation.md @@ -0,0 +1,47 @@ +--- +error: true +--- + +# batching validation + +```graphql @config +schema @upstream(httpCache: 42, batch: {delay: 1}) { + query: Query +} + +type User { + id: Int + name: String +} + +type Post { + id: Int + title: String + body: String +} + +type Query { + user(id: Int!): User + @http( + url: "http://jsonplaceholder.typicode.com/users" + method: POST + body: {uId: "{{.args.id}}", userId: "{{.args.id}}"} + batchKey: ["id"] + ) + posts: [Post] @http(url: "http://jsonplaceholder.typicode.com/posts", batchKey: ["id"]) + userWithId(id: Int!): User + @http( + url: "http://jsonplaceholder.typicode.com/users" + method: POST + body: {uId: "uId", userId: "userId"} + batchKey: ["id"] + ) + userWithIdTest(id: Int!): User + @http( + url: "http://jsonplaceholder.typicode.com/users" + method: PUT + body: {uId: "uId", userId: "userId"} + batchKey: ["id"] + ) +} +``` diff --git a/tests/execution/body-batching-cases.md b/tests/execution/body-batching-cases.md new file mode 100644 index 0000000000..a8e124a49a --- /dev/null +++ b/tests/execution/body-batching-cases.md @@ -0,0 +1,154 @@ +# Batching default + +```graphql @config +schema @server(port: 8000) @upstream(httpCache: 42, batch: {delay: 1}) { + query: Query +} + +type Query { + user: User @http(url: "http://jsonplaceholder.typicode.com/users/1") + users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") + posts: [Post] @http(url: "http://jsonplaceholder.typicode.com/posts") + foo: [Foo] @http(url: "http://jsonplaceholder.typicode.com/foo") +} + +type Foo { + a: Int + b: Int + bar: Bar + @http(url: "http://jsonplaceholder.typicode.com/bar", method: POST, body: {id: "{{.value.a}}"}, batchKey: ["a"]) +} + +type Bar { + a: Int + b: Int +} + +type User { + id: Int! + name: String! + email: String! + post: Post + @http( + url: "http://jsonplaceholder.typicode.com/posts" + method: POST + body: {userId: "{{.value.id}}", title: "title", body: "body"} + batchKey: ["userId"] + ) +} + +type Post { + id: Int! + userId: Int! + title: String! + body: String! + user: User + @http( + url: "http://jsonplaceholder.typicode.com/users" + method: POST + body: {key: "id", value: "{{.value.userId}}"} + batchKey: ["id"] + ) +} +``` + +```yml @mock +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users + response: + status: 200 + body: + - id: 1 + name: user-1 + email: user-1@gmail.com + - id: 2 + name: user-2 + email: user-2@gmail.com +- request: + method: POST + url: http://jsonplaceholder.typicode.com/posts + body: [{"userId": "1", "title": "title", "body": "body"}, {"userId": "2", "title": "title", "body": "body"}] + response: + status: 200 + body: + - id: 1 + userId: 1 + title: user-1 + body: user-1@gmail.com + - id: 2 + userId: 2 + title: user-2 + body: user-2@gmail.com + +- request: + method: GET + url: http://jsonplaceholder.typicode.com/posts + response: + status: 200 + body: + - id: 1 + userId: 1 + title: user-1 + body: user-1@gmail.com + - id: 2 + userId: 2 + title: user-2 + body: user-2@gmail.com + +- request: + method: POST + url: http://jsonplaceholder.typicode.com/users + body: [{"key": "id", "value": "1"}, {"key": "id", "value": "2"}] + response: + status: 200 + body: + - id: 1 + name: user-1 + email: user-1@gmail.com + - id: 2 + userId: 2 + name: user-2 + email: user-2@gmail.com + +- request: + method: GET + url: http://jsonplaceholder.typicode.com/foo + expectedHits: 1 + response: + status: 200 + body: + - a: 11 + b: 12 + - a: 21 + b: 22 + +- request: + method: POST + url: http://jsonplaceholder.typicode.com/bar + body: [{"id": "11"}, {"id": "21"}] + response: + status: 200 + body: + - a: 11 + b: 12 + - a: 21 + b: 22 +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: query { users { id name post { id title userId } } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: query { posts { id title user { id name } } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: query { foo { a b bar { a b } } } +``` diff --git a/tests/execution/body-batching.md b/tests/execution/body-batching.md new file mode 100644 index 0000000000..a5fce54fce --- /dev/null +++ b/tests/execution/body-batching.md @@ -0,0 +1,114 @@ +# Batching post + +```graphql @config +schema + @server(port: 8000, queryValidation: false) + @upstream(httpCache: 42, batch: {maxSize: 1000, delay: 1, headers: []}) { + query: Query +} + +type Query { + users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") +} + +type Post { + id: Int + title: String + body: String + userId: Int! +} + +type User { + id: Int! + name: String! + posts: [Post] + @http( + url: "https://jsonplaceholder.typicode.com/posts" + method: POST + body: {userId: "{{.value.id}}", title: "foo", body: "bar"} + batchKey: ["userId"] + ) + comments: [Comment] + @http( + url: "https://jsonplaceholder.typicode.com/comments" + method: POST + body: {title: "foo", body: "bar", meta: {information: {userId: "{{.value.id}}"}}} + batchKey: ["userId"] + ) +} + +type Comment { + id: Int +} +``` + +```yml @mock +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users + expectedHits: 2 + response: + status: 200 + body: + - id: 1 + name: user-1 + - id: 2 + name: user-2 + - id: 3 + name: user-3 +- request: + method: POST + url: https://jsonplaceholder.typicode.com/posts + body: + [ + {"userId": "1", "title": "foo", "body": "bar"}, + {"userId": "2", "title": "foo", "body": "bar"}, + {"userId": "3", "title": "foo", "body": "bar"}, + ] + response: + status: 200 + body: + - id: 1 + title: foo + body: bar + userId: 1 + - id: 2 + title: foo + body: bar + userId: 2 + - id: 3 + title: foo + body: bar + userId: 3 + +- request: + method: POST + url: https://jsonplaceholder.typicode.com/comments + body: + [ + {"title": "foo", "body": "bar", "meta": {"information": {"userId": "1"}}}, + {"title": "foo", "body": "bar", "meta": {"information": {"userId": "2"}}}, + {"title": "foo", "body": "bar", "meta": {"information": {"userId": "3"}}}, + ] + response: + status: 200 + body: + - id: 1 + userId: 1 + - id: 2 + userId: 2 + - id: 3 + userId: 3 +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: query { users { id posts { userId title } } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: query { users { id comments { id } } } +``` diff --git a/tests/execution/call-mutation.md b/tests/execution/call-mutation.md index 109a86eee2..bcda2cd7b0 100644 --- a/tests/execution/call-mutation.md +++ b/tests/execution/call-mutation.md @@ -83,7 +83,7 @@ type User { - request: method: PATCH url: http://jsonplaceholder.typicode.com/users/1 - body: {"postId": 1} + body: '{"postId":1}' response: status: 200 body: