diff --git a/Cargo.lock b/Cargo.lock index 2be24067..791450a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1911,6 +1911,7 @@ dependencies = [ "itertools", "ndc-models", "serde_json", + "smol_str", ] [[package]] diff --git a/arion-compose/integration-tests.nix b/arion-compose/integration-tests.nix index 1eb25fd1..6e45df8d 100644 --- a/arion-compose/integration-tests.nix +++ b/arion-compose/integration-tests.nix @@ -9,13 +9,14 @@ { pkgs, config, ... }: let + connector-port = "7130"; + connector-chinook-port = "7131"; + engine-port = "7100"; + services = import ./integration-test-services.nix { - inherit pkgs engine-port; + inherit pkgs connector-port connector-chinook-port engine-port; map-host-ports = false; }; - - connector-port = "7130"; - engine-port = "7100"; in { project.name = "mongodb-connector-integration-tests"; @@ -24,6 +25,7 @@ in test = import ./services/integration-tests.nix { inherit pkgs; connector-url = "http://connector:${connector-port}/"; + connector-chinook-url = "http://connector-chinook:${connector-chinook-port}/"; engine-graphql-url = "http://engine:${engine-port}/graphql"; service.depends_on = { connector.condition = "service_healthy"; diff --git a/arion-compose/services/integration-tests.nix b/arion-compose/services/integration-tests.nix index fa99283a..e25d3770 100644 --- a/arion-compose/services/integration-tests.nix +++ b/arion-compose/services/integration-tests.nix @@ -1,5 +1,6 @@ { pkgs , connector-url +, connector-chinook-url , engine-graphql-url , service ? { } # additional options to customize this service configuration }: @@ -14,6 +15,7 @@ let ]; environment = { CONNECTOR_URL = connector-url; + CONNECTOR_CHINOOK_URL = connector-chinook-url; ENGINE_GRAPHQL_URL = engine-graphql-url; INSTA_WORKSPACE_ROOT = repo-source-mount-point; MONGODB_IMAGE = builtins.getEnv "MONGODB_IMAGE"; diff --git a/crates/integration-tests/src/connector.rs b/crates/integration-tests/src/connector.rs index b7d6807e..858b668c 100644 --- a/crates/integration-tests/src/connector.rs +++ b/crates/integration-tests/src/connector.rs @@ -1,19 +1,36 @@ use ndc_models::{ErrorResponse, QueryRequest, QueryResponse}; -use ndc_test_helpers::QueryRequestBuilder; use reqwest::Client; use serde::{Deserialize, Serialize}; +use url::Url; -use crate::get_connector_url; +use crate::{get_connector_chinook_url, get_connector_url}; #[derive(Clone, Debug, Serialize)] #[serde(transparent)] pub struct ConnectorQueryRequest { + #[serde(skip)] + connector: Connector, query_request: QueryRequest, } +#[derive(Clone, Copy, Debug)] +pub enum Connector { + Chinook, + SampleMflix, +} + +impl Connector { + fn url(self) -> anyhow::Result { + match self { + Connector::Chinook => get_connector_chinook_url(), + Connector::SampleMflix => get_connector_url(), + } + } +} + impl ConnectorQueryRequest { pub async fn run(&self) -> anyhow::Result { - let connector_url = get_connector_url()?; + let connector_url = self.connector.url()?; let client = Client::new(); let response = client .post(connector_url.join("query")?) @@ -26,23 +43,14 @@ impl ConnectorQueryRequest { } } -impl From for ConnectorQueryRequest { - fn from(query_request: QueryRequest) -> Self { - ConnectorQueryRequest { query_request } - } -} - -impl From for ConnectorQueryRequest { - fn from(builder: QueryRequestBuilder) -> Self { - let request: QueryRequest = builder.into(); - request.into() - } -} - pub async fn run_connector_query( - request: impl Into, + connector: Connector, + request: impl Into, ) -> anyhow::Result { - let request: ConnectorQueryRequest = request.into(); + let request = ConnectorQueryRequest { + connector, + query_request: request.into(), + }; request.run().await } diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index 9044753e..42cb5c8e 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -18,6 +18,7 @@ pub use self::connector::{run_connector_query, ConnectorQueryRequest}; pub use self::graphql::{graphql_query, GraphQLRequest, GraphQLResponse}; const CONNECTOR_URL: &str = "CONNECTOR_URL"; +const CONNECTOR_CHINOOK_URL: &str = "CONNECTOR_CHINOOK_URL"; const ENGINE_GRAPHQL_URL: &str = "ENGINE_GRAPHQL_URL"; fn get_connector_url() -> anyhow::Result { @@ -26,6 +27,12 @@ fn get_connector_url() -> anyhow::Result { Ok(url) } +fn get_connector_chinook_url() -> anyhow::Result { + let input = env::var(CONNECTOR_CHINOOK_URL).map_err(|_| anyhow!("please set {CONNECTOR_CHINOOK_URL} to the the base URL of a running MongoDB connector instance"))?; + let url = Url::parse(&input)?; + Ok(url) +} + fn get_graphql_url() -> anyhow::Result { env::var(ENGINE_GRAPHQL_URL).map_err(|_| anyhow!("please set {ENGINE_GRAPHQL_URL} to the GraphQL endpoint of a running GraphQL Engine server")) } diff --git a/crates/integration-tests/src/tests/basic.rs b/crates/integration-tests/src/tests/basic.rs index 984614bb..eea422a0 100644 --- a/crates/integration-tests/src/tests/basic.rs +++ b/crates/integration-tests/src/tests/basic.rs @@ -1,5 +1,6 @@ use crate::graphql_query; use insta::assert_yaml_snapshot; +use serde_json::json; #[tokio::test] async fn runs_a_query() -> anyhow::Result<()> { @@ -22,3 +23,50 @@ async fn runs_a_query() -> anyhow::Result<()> { ); Ok(()) } + +#[tokio::test] +async fn filters_by_date() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query ($dateInput: Date) { + movies( + order_by: {id: Asc}, + where: {released: {_gt: $dateInput}} + ) { + title + released + } + } + "# + ) + .variables(json!({ "dateInput": "2016-03-01T00:00Z" })) + .run() + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn selects_array_within_array() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + artistsWithAlbumsAndTracks(limit: 1, order_by: {id: Asc}) { + name + albums { + title + tracks { + name + } + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/local_relationship.rs b/crates/integration-tests/src/tests/local_relationship.rs index 70ce7162..d254c0a2 100644 --- a/crates/integration-tests/src/tests/local_relationship.rs +++ b/crates/integration-tests/src/tests/local_relationship.rs @@ -135,3 +135,50 @@ async fn sorts_by_field_of_related_collection() -> anyhow::Result<()> { ); Ok(()) } + +#[tokio::test] +async fn looks_up_the_same_relation_twice_with_different_fields() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + artist(limit: 2, order_by: {id: Asc}) { + albums1: albums(order_by: {title: Asc}) { + title + } + albums2: albums(order_by: {title: Asc}) { + tracks(order_by: {name: Asc}) { + name + } + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn queries_through_relationship_with_null_value() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + comments(where: {id: {_eq: "5a9427648b0beebeb69579cc"}}) { # this comment does not have a matching movie + movie { + comments { + email + } + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/native_query.rs b/crates/integration-tests/src/tests/native_query.rs index 1e929ee5..aa9ec513 100644 --- a/crates/integration-tests/src/tests/native_query.rs +++ b/crates/integration-tests/src/tests/native_query.rs @@ -1,5 +1,6 @@ -use crate::graphql_query; +use crate::{connector::Connector, graphql_query, run_connector_query}; use insta::assert_yaml_snapshot; +use ndc_test_helpers::{asc, binop, field, query, query_request, target, variable}; #[tokio::test] async fn runs_native_query_with_function_representation() -> anyhow::Result<()> { @@ -51,3 +52,35 @@ async fn runs_native_query_with_collection_representation() -> anyhow::Result<() ); Ok(()) } + +#[tokio::test] +async fn runs_native_query_with_variable_sets() -> anyhow::Result<()> { + // Skip this test in MongoDB 5 because the example fails there. We're getting an error: + // + // > Kind: Command failed: Error code 5491300 (Location5491300): $documents' is not allowed in user requests, labels: {} + // + // This means that remote joins are not working in MongoDB 5 + if let Ok(image) = std::env::var("MONGODB_IMAGE") { + if image == "mongo:5" { + return Ok(()); + } + } + + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request() + .variables([[("count", 1)], [("count", 2)], [("count", 3)]]) + .collection("title_word_frequency") + .query( + query() + .predicate(binop("_eq", target!("count"), variable!(count))) + .order_by([asc!("_id")]) + .limit(20) + .fields([field!("_id"), field!("count")]), + ) + ) + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/remote_relationship.rs b/crates/integration-tests/src/tests/remote_relationship.rs index c4a99608..fa1202c9 100644 --- a/crates/integration-tests/src/tests/remote_relationship.rs +++ b/crates/integration-tests/src/tests/remote_relationship.rs @@ -1,6 +1,6 @@ -use crate::{graphql_query, run_connector_query}; +use crate::{connector::Connector, graphql_query, run_connector_query}; use insta::assert_yaml_snapshot; -use ndc_test_helpers::{binop, field, query, query_request, target, variable}; +use ndc_test_helpers::{and, asc, binop, field, query, query_request, target, variable}; use serde_json::json; #[tokio::test] @@ -53,6 +53,7 @@ async fn handles_request_with_single_variable_set() -> anyhow::Result<()> { assert_yaml_snapshot!( run_connector_query( + Connector::SampleMflix, query_request() .collection("movies") .variables([[("id", json!("573a1390f29313caabcd50e5"))]]) @@ -66,3 +67,43 @@ async fn handles_request_with_single_variable_set() -> anyhow::Result<()> { ); Ok(()) } + +#[tokio::test] +async fn variable_used_in_multiple_type_contexts() -> anyhow::Result<()> { + // Skip this test in MongoDB 5 because the example fails there. We're getting an error: + // + // > Kind: Command failed: Error code 5491300 (Location5491300): $documents' is not allowed in user requests, labels: {} + // + // This means that remote joins are not working in MongoDB 5 + if let Ok(image) = std::env::var("MONGODB_IMAGE") { + if image == "mongo:5" { + return Ok(()); + } + } + + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request() + .variables([[("dateInput", "2015-09-15T00:00Z")]]) + .collection("movies") + .query( + query() + .predicate(and([ + binop("_gt", target!("released"), variable!(dateInput)), // type is date + binop("_gt", target!("lastupdated"), variable!(dateInput)), // type is string + ])) + .order_by([asc!("_id")]) + .limit(20) + .fields([ + field!("_id"), + field!("title"), + field!("released"), + field!("lastupdated") + ]), + ) + ) + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__basic__filters_by_date.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__basic__filters_by_date.snap new file mode 100644 index 00000000..c86ffa15 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__basic__filters_by_date.snap @@ -0,0 +1,11 @@ +--- +source: crates/integration-tests/src/tests/basic.rs +expression: "graphql_query(r#\"\n query ($dateInput: Date) {\n movies(\n order_by: {id: Asc},\n where: {released: {_gt: $dateInput}}\n ) {\n title\n released\n }\n }\n \"#).variables(json!({\n \"dateInput\": \"2016-03-01T00:00Z\"\n })).run().await?" +--- +data: + movies: + - title: Knight of Cups + released: "2016-03-04T00:00:00.000000000Z" + - title: The Treasure + released: "2016-03-23T00:00:00.000000000Z" +errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__basic__selects_array_within_array.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__basic__selects_array_within_array.snap new file mode 100644 index 00000000..140b5edf --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__basic__selects_array_within_array.snap @@ -0,0 +1,31 @@ +--- +source: crates/integration-tests/src/tests/basic.rs +expression: "graphql_query(r#\"\n query {\n artistsWithAlbumsAndTracks(limit: 1, order_by: {id: Asc}) {\n name\n albums {\n title\n tracks {\n name\n }\n }\n }\n }\n \"#).run().await?" +--- +data: + artistsWithAlbumsAndTracks: + - name: AC/DC + albums: + - title: For Those About To Rock We Salute You + tracks: + - name: Breaking The Rules + - name: C.O.D. + - name: Evil Walks + - name: For Those About To Rock (We Salute You) + - name: Inject The Venom + - name: "Let's Get It Up" + - name: Night Of The Long Knives + - name: Put The Finger On You + - name: Snowballed + - name: Spellbound + - title: Let There Be Rock + tracks: + - name: Bad Boy Boogie + - name: Dog Eat Dog + - name: Go Down + - name: "Hell Ain't A Bad Place To Be" + - name: Let There Be Rock + - name: Overdose + - name: Problem Child + - name: Whole Lotta Rosie +errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__looks_up_the_same_relation_twice_with_different_fields.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__looks_up_the_same_relation_twice_with_different_fields.snap new file mode 100644 index 00000000..839d6d19 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__looks_up_the_same_relation_twice_with_different_fields.snap @@ -0,0 +1,46 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "graphql_query(r#\"\n {\n artist(limit: 2, order_by: {id: Asc}) {\n albums1: albums(order_by: {title: Asc}) {\n title\n }\n albums2: albums {\n tracks(order_by: {name: Asc}) {\n name\n }\n }\n }\n }\n \"#).run().await?" +--- +data: + artist: + - albums1: + - title: For Those About To Rock We Salute You + - title: Let There Be Rock + albums2: + - tracks: + - name: Breaking The Rules + - name: C.O.D. + - name: Evil Walks + - name: For Those About To Rock (We Salute You) + - name: Inject The Venom + - name: "Let's Get It Up" + - name: Night Of The Long Knives + - name: Put The Finger On You + - name: Snowballed + - name: Spellbound + - tracks: + - name: Bad Boy Boogie + - name: Dog Eat Dog + - name: Go Down + - name: "Hell Ain't A Bad Place To Be" + - name: Let There Be Rock + - name: Overdose + - name: Problem Child + - name: Whole Lotta Rosie + - albums1: + - title: The Best Of Buddy Guy - The Millenium Collection + albums2: + - tracks: + - name: First Time I Met The Blues + - name: Keep It To Myself (Aka Keep It To Yourself) + - name: Leave My Girl Alone + - name: Let Me Love You Baby + - name: My Time After Awhile + - name: Pretty Baby + - name: She Suits Me To A Tee + - name: Stone Crazy + - name: "Talkin' 'Bout Women Obviously" + - name: Too Many Ways (Alternate) + - name: When My Left Eye Jumps +errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__queries_through_relationship_with_null_value.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__queries_through_relationship_with_null_value.snap new file mode 100644 index 00000000..6c043f03 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__queries_through_relationship_with_null_value.snap @@ -0,0 +1,8 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "graphql_query(r#\"\n query {\n comments(where: {id: {_eq: \"5a9427648b0beebeb69579cc\"}}) { # this comment does not have a matching movie\n movie {\n comments {\n email\n }\n } \n }\n }\n \"#).run().await?" +--- +data: + comments: + - movie: ~ +errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__sorts_by_two_fields_of_related_collection.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__sorts_by_two_fields_of_related_collection.snap new file mode 100644 index 00000000..df447056 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__sorts_by_two_fields_of_related_collection.snap @@ -0,0 +1,17 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "graphql_query(r#\"\n query {\n comments(\n limit: 10\n order_by: [{movie: {title: Asc}}, {date: Asc}]\n where: {movie: {rated: {_eq: \"G\"}, released: {_gt: \"2015-01-01T00:00Z\"}}}\n ) {\n movie {\n title\n year\n released\n }\n text\n }\n }\n \"#).run().await?" +--- +data: + comments: + - movie: + title: Maya the Bee Movie + year: 2014 + released: "2015-03-08T00:00:00.000000000Z" + text: Pariatur eius nulla dolor voluptatum ab. A amet delectus repellat consequuntur eius illum. Optio voluptates dignissimos ipsam saepe eos provident ut. Incidunt eum nemo voluptatem velit similique. + - movie: + title: Maya the Bee Movie + year: 2014 + released: "2015-03-08T00:00:00.000000000Z" + text: Error doloribus doloremque commodi aut porro nesciunt. Qui dicta incidunt cumque. Quidem ea officia aperiam est. Laboriosam explicabo eum ipsum quam tempore iure tenetur. +errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__native_query__runs_native_query_with_variable_sets.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__native_query__runs_native_query_with_variable_sets.snap new file mode 100644 index 00000000..6ebac5f2 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__native_query__runs_native_query_with_variable_sets.snap @@ -0,0 +1,127 @@ +--- +source: crates/integration-tests/src/tests/native_query.rs +expression: "run_connector_query(query_request().variables([[(\"count\", 1)], [(\"count\", 2)],\n [(\"count\",\n 3)]]).collection(\"title_word_frequency\").query(query().predicate(binop(\"_eq\",\n target!(\"count\"),\n variable!(count))).order_by([asc!(\"_id\")]).limit(20).fields([field!(\"_id\"),\n field!(\"count\")]))).await?" +--- +- rows: + - _id: "!Women" + count: 1 + - _id: "#$*!" + count: 1 + - _id: "#9" + count: 1 + - _id: "#chicagoGirl:" + count: 1 + - _id: $ + count: 1 + - _id: $9.99 + count: 1 + - _id: $ellebrity + count: 1 + - _id: "'...And" + count: 1 + - _id: "'36" + count: 1 + - _id: "'42" + count: 1 + - _id: "'44" + count: 1 + - _id: "'51" + count: 1 + - _id: "'63" + count: 1 + - _id: "'66" + count: 1 + - _id: "'69" + count: 1 + - _id: "'70" + count: 1 + - _id: "'71" + count: 1 + - _id: "'73" + count: 1 + - _id: "'79" + count: 1 + - _id: "'81" + count: 1 +- rows: + - _id: "'45" + count: 2 + - _id: "'Round" + count: 2 + - _id: "'Til" + count: 2 + - _id: (A + count: 2 + - _id: (And + count: 2 + - _id: (Yellow) + count: 2 + - _id: "...And" + count: 2 + - _id: ".45" + count: 2 + - _id: "1,000" + count: 2 + - _id: 100% + count: 2 + - _id: "102" + count: 2 + - _id: "1138" + count: 2 + - _id: "117:" + count: 2 + - _id: 11th + count: 2 + - _id: "13th:" + count: 2 + - _id: "14" + count: 2 + - _id: "1896" + count: 2 + - _id: "1900" + count: 2 + - _id: "1980" + count: 2 + - _id: "1987" + count: 2 +- rows: + - _id: "#1" + count: 3 + - _id: "'n" + count: 3 + - _id: "'n'" + count: 3 + - _id: (Not) + count: 3 + - _id: "100" + count: 3 + - _id: 10th + count: 3 + - _id: "15" + count: 3 + - _id: "174" + count: 3 + - _id: "23" + count: 3 + - _id: 3-D + count: 3 + - _id: "42" + count: 3 + - _id: "420" + count: 3 + - _id: "72" + count: 3 + - _id: Abandoned + count: 3 + - _id: Abendland + count: 3 + - _id: Absence + count: 3 + - _id: Absent + count: 3 + - _id: Abu + count: 3 + - _id: Accident + count: 3 + - _id: Accidental + count: 3 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__variable_used_in_multiple_type_contexts.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__variable_used_in_multiple_type_contexts.snap new file mode 100644 index 00000000..f69a5b00 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__variable_used_in_multiple_type_contexts.snap @@ -0,0 +1,33 @@ +--- +source: crates/integration-tests/src/tests/remote_relationship.rs +expression: "run_connector_query(query_request().variables([[(\"dateInput\",\n \"2015-09-15T00:00Z\")]]).collection(\"movies\").query(query().predicate(and([binop(\"_gt\",\n target!(\"released\"), variable!(dateInput)),\n binop(\"_gt\", target!(\"lastupdated\"),\n variable!(dateInput))])).order_by([asc!(\"_id\")]).limit(20).fields([field!(\"_id\"),\n field!(\"title\"), field!(\"released\"),\n field!(\"lastupdated\")]))).await?" +--- +- rows: + - _id: 573a13d3f29313caabd967ef + lastupdated: "2015-09-17 03:51:47.073000000" + released: "2015-11-01T00:00:00.000000000Z" + title: Another World + - _id: 573a13eaf29313caabdcfa99 + lastupdated: "2015-09-16 07:39:43.980000000" + released: "2015-10-02T00:00:00.000000000Z" + title: Sicario + - _id: 573a13ebf29313caabdd0792 + lastupdated: "2015-09-16 13:01:10.653000000" + released: "2015-11-04T00:00:00.000000000Z" + title: April and the Extraordinary World + - _id: 573a13f0f29313caabdd9b5d + lastupdated: "2015-09-17 04:41:09.897000000" + released: "2015-09-17T00:00:00.000000000Z" + title: The Wait + - _id: 573a13f1f29313caabddc788 + lastupdated: "2015-09-17 03:17:32.967000000" + released: "2015-12-18T00:00:00.000000000Z" + title: Son of Saul + - _id: 573a13f2f29313caabddd3b6 + lastupdated: "2015-09-17 02:59:54.573000000" + released: "2016-01-13T00:00:00.000000000Z" + title: Bang Gang (A Modern Love Story) + - _id: 573a13f4f29313caabde0bfd + lastupdated: "2015-09-17 02:00:44.673000000" + released: "2016-02-19T00:00:00.000000000Z" + title: Shut In diff --git a/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs b/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs index 05a75b5c..5dff0be0 100644 --- a/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs +++ b/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs @@ -21,6 +21,9 @@ pub enum JsonToBsonError { #[error("error converting \"{1}\" to type, \"{0:?}\": {2}")] ConversionErrorWithContext(Type, Value, #[source] anyhow::Error), + #[error("error parsing \"{0}\" as a date. Date values should be in ISO 8601 format with a time component, like `2016-01-01T00:00Z`. Underlying error: {1}")] + DateConversionErrorWithContext(Value, #[source] anyhow::Error), + #[error("cannot use value, \"{0:?}\", in position of type, \"{1:?}\"")] IncompatibleType(Type, Value), @@ -173,12 +176,8 @@ fn convert_nullable(underlying_type: &Type, value: Value) -> Result { } fn convert_date(value: &str) -> Result { - let date = OffsetDateTime::parse(value, &Iso8601::DEFAULT).map_err(|err| { - JsonToBsonError::ConversionErrorWithContext( - Type::Scalar(MongoScalarType::Bson(BsonScalarType::Date)), - Value::String(value.to_owned()), - err.into(), - ) + let date = OffsetDateTime::parse(value, &Iso8601::PARSING).map_err(|err| { + JsonToBsonError::DateConversionErrorWithContext(Value::String(value.to_owned()), err.into()) })?; Ok(Bson::DateTime(bson::DateTime::from_system_time( date.into(), @@ -383,4 +382,16 @@ mod tests { assert_eq!(actual, bson!({})); Ok(()) } + + #[test] + fn converts_string_input_to_date() -> anyhow::Result<()> { + let input = json!("2016-01-01T00:00Z"); + let actual = json_to_bson( + &Type::Scalar(MongoScalarType::Bson(BsonScalarType::Date)), + input, + )?; + let expected = Bson::DateTime(bson::DateTime::from_millis(1_451_606_400_000)); + assert_eq!(actual, expected); + Ok(()) + } } diff --git a/crates/ndc-test-helpers/Cargo.toml b/crates/ndc-test-helpers/Cargo.toml index 99349435..cdc1bcc1 100644 --- a/crates/ndc-test-helpers/Cargo.toml +++ b/crates/ndc-test-helpers/Cargo.toml @@ -8,3 +8,4 @@ indexmap = { workspace = true } itertools = { workspace = true } ndc-models = { workspace = true } serde_json = "1" +smol_str = "*" diff --git a/crates/ndc-test-helpers/src/lib.rs b/crates/ndc-test-helpers/src/lib.rs index 1e30c2ca..706cefd6 100644 --- a/crates/ndc-test-helpers/src/lib.rs +++ b/crates/ndc-test-helpers/src/lib.rs @@ -9,6 +9,7 @@ mod exists_in_collection; mod expressions; mod field; mod object_type; +mod order_by; mod path_element; mod query_response; mod relationships; @@ -24,6 +25,7 @@ use ndc_models::{ // Export this crate's reference to ndc_models so that we can use this reference in macros. pub extern crate ndc_models; +pub extern crate smol_str; pub use collection_info::*; pub use comparison_target::*; @@ -32,6 +34,7 @@ pub use exists_in_collection::*; pub use expressions::*; pub use field::*; pub use object_type::*; +pub use order_by::*; pub use path_element::*; pub use query_response::*; pub use relationships::*; @@ -182,8 +185,13 @@ impl QueryBuilder { self } - pub fn order_by(mut self, elements: Vec) -> Self { - self.order_by = Some(OrderBy { elements }); + pub fn order_by( + mut self, + elements: impl IntoIterator>, + ) -> Self { + self.order_by = Some(OrderBy { + elements: elements.into_iter().map(Into::into).collect(), + }); self } diff --git a/crates/ndc-test-helpers/src/order_by.rs b/crates/ndc-test-helpers/src/order_by.rs new file mode 100644 index 00000000..9ea8c778 --- /dev/null +++ b/crates/ndc-test-helpers/src/order_by.rs @@ -0,0 +1,27 @@ +#[macro_export] +macro_rules! asc { + ($name:literal) => { + $crate::ndc_models::OrderByElement { + order_direction: $crate::ndc_models::OrderDirection::Asc, + target: $crate::ndc_models::OrderByTarget::Column { + name: $crate::ndc_models::FieldName::new($crate::smol_str::SmolStr::new($name)), + field_path: None, + path: vec![], + }, + } + }; +} + +#[macro_export] +macro_rules! desc { + ($name:literal) => { + $crate::ndc_models::OrderByElement { + order_direction: $crate::ndc_models::OrderDirection::Desc, + target: $crate::ndc_models::OrderByTarget::Column { + name: $crate::ndc_models::FieldName::new($crate::smol_str::SmolStr::new($name)), + field_path: None, + path: vec![], + }, + } + }; +} diff --git a/fixtures/hasura/chinook/connector/chinook/native_queries/artists_with_albums_and_tracks.json b/fixtures/hasura/chinook/connector/chinook/native_queries/artists_with_albums_and_tracks.json new file mode 100644 index 00000000..542366fe --- /dev/null +++ b/fixtures/hasura/chinook/connector/chinook/native_queries/artists_with_albums_and_tracks.json @@ -0,0 +1,71 @@ +{ + "name": "artists_with_albums_and_tracks", + "representation": "collection", + "inputCollection": "Artist", + "description": "combines artist, albums, and tracks into a single document per artist", + "resultDocumentType": "ArtistWithAlbumsAndTracks", + "objectTypes": { + "ArtistWithAlbumsAndTracks": { + "fields": { + "_id": { "type": { "scalar": "objectId" } }, + "Name": { "type": { "scalar": "string" } }, + "Albums": { "type": { "arrayOf": { "object": "AlbumWithTracks" } } } + } + }, + "AlbumWithTracks": { + "fields": { + "_id": { "type": { "scalar": "objectId" } }, + "Title": { "type": { "scalar": "string" } }, + "Tracks": { "type": { "arrayOf": { "object": "Track" } } } + } + } + }, + "pipeline": [ + { + "$lookup": { + "from": "Album", + "localField": "ArtistId", + "foreignField": "ArtistId", + "as": "Albums", + "pipeline": [ + { + "$lookup": { + "from": "Track", + "localField": "AlbumId", + "foreignField": "AlbumId", + "as": "Tracks", + "pipeline": [ + { + "$sort": { + "Name": 1 + } + } + ] + } + }, + { + "$replaceWith": { + "_id": "$_id", + "Title": "$Title", + "Tracks": "$Tracks" + } + }, + { + "$sort": { + "Title": 1 + } + } + ] + } + }, + { + "$replaceWith": { + "_id": "$_id", + "Name": "$Name", + "Albums": "$Albums" + } + } + ] +} + + diff --git a/fixtures/hasura/chinook/metadata/ArtistsWithAlbumsAndTracks.hml b/fixtures/hasura/chinook/metadata/ArtistsWithAlbumsAndTracks.hml new file mode 100644 index 00000000..43308e50 --- /dev/null +++ b/fixtures/hasura/chinook/metadata/ArtistsWithAlbumsAndTracks.hml @@ -0,0 +1,145 @@ +--- +kind: ObjectType +version: v1 +definition: + name: AlbumWithTracks + fields: + - name: id + type: Chinook_ObjectId! + - name: title + type: String! + - name: tracks + type: "[Track!]!" + graphql: + typeName: Chinook_AlbumWithTracks + inputTypeName: Chinook_AlbumWithTracksInput + dataConnectorTypeMapping: + - dataConnectorName: chinook + dataConnectorObjectType: AlbumWithTracks + fieldMapping: + id: + column: + name: _id + title: + column: + name: Title + tracks: + column: + name: Tracks + +--- +kind: TypePermissions +version: v1 +definition: + typeName: AlbumWithTracks + permissions: + - role: admin + output: + allowedFields: + - id + - title + - tracks + +--- +kind: ObjectType +version: v1 +definition: + name: ArtistWithAlbumsAndTracks + fields: + - name: id + type: Chinook_ObjectId! + - name: albums + type: "[AlbumWithTracks!]!" + - name: name + type: String! + graphql: + typeName: Chinook_ArtistWithAlbumsAndTracks + inputTypeName: Chinook_ArtistWithAlbumsAndTracksInput + dataConnectorTypeMapping: + - dataConnectorName: chinook + dataConnectorObjectType: ArtistWithAlbumsAndTracks + fieldMapping: + id: + column: + name: _id + albums: + column: + name: Albums + name: + column: + name: Name + +--- +kind: TypePermissions +version: v1 +definition: + typeName: ArtistWithAlbumsAndTracks + permissions: + - role: admin + output: + allowedFields: + - id + - albums + - name + +--- +kind: ObjectBooleanExpressionType +version: v1 +definition: + name: ArtistWithAlbumsAndTracksBoolExp + objectType: ArtistWithAlbumsAndTracks + dataConnectorName: chinook + dataConnectorObjectType: ArtistWithAlbumsAndTracks + comparableFields: + - fieldName: id + operators: + enableAll: true + - fieldName: albums + operators: + enableAll: true + - fieldName: name + operators: + enableAll: true + graphql: + typeName: Chinook_ArtistWithAlbumsAndTracksBoolExp + +--- +kind: Model +version: v1 +definition: + name: ArtistsWithAlbumsAndTracks + objectType: ArtistWithAlbumsAndTracks + source: + dataConnectorName: chinook + collection: artists_with_albums_and_tracks + filterExpressionType: ArtistWithAlbumsAndTracksBoolExp + orderableFields: + - fieldName: id + orderByDirections: + enableAll: true + - fieldName: albums + orderByDirections: + enableAll: true + - fieldName: name + orderByDirections: + enableAll: true + graphql: + selectMany: + queryRootField: artistsWithAlbumsAndTracks + selectUniques: + - queryRootField: artistsWithAlbumsAndTracksById + uniqueIdentifier: + - id + orderByExpressionType: Chinook_ArtistsWithAlbumsAndTracksOrderBy + description: combines artist, albums, and tracks into a single document per artist + +--- +kind: ModelPermissions +version: v1 +definition: + modelName: ArtistsWithAlbumsAndTracks + permissions: + - role: admin + select: + filter: null + diff --git a/fixtures/hasura/chinook/metadata/chinook.hml b/fixtures/hasura/chinook/metadata/chinook.hml index 86f633b4..e242eade 100644 --- a/fixtures/hasura/chinook/metadata/chinook.hml +++ b/fixtures/hasura/chinook/metadata/chinook.hml @@ -536,6 +536,22 @@ definition: type: type: named name: String + AlbumWithTracks: + fields: + _id: + type: + type: named + name: ObjectId + Title: + type: + type: named + name: String + Tracks: + type: + type: array + element_type: + type: named + name: Track Artist: description: Object type for collection Artist fields: @@ -553,6 +569,22 @@ definition: underlying_type: type: named name: String + ArtistWithAlbumsAndTracks: + fields: + _id: + type: + type: named + name: ObjectId + Albums: + type: + type: array + element_type: + type: named + name: AlbumWithTracks + Name: + type: + type: named + name: String Customer: description: Object type for collection Customer fields: @@ -1017,6 +1049,15 @@ definition: unique_columns: - _id foreign_keys: {} + - name: artists_with_albums_and_tracks + description: combines artist, albums, and tracks into a single document per artist + arguments: {} + type: ArtistWithAlbumsAndTracks + uniqueness_constraints: + artists_with_albums_and_tracks_id: + unique_columns: + - _id + foreign_keys: {} functions: [] procedures: - name: insertArtist diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/movies.json b/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/movies.json index 96784456..b7dc4ca5 100644 --- a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/movies.json +++ b/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/movies.json @@ -305,4 +305,4 @@ } } } -} \ No newline at end of file +} diff --git a/fixtures/hasura/sample_mflix/metadata/models/Movies.hml b/fixtures/hasura/sample_mflix/metadata/models/Movies.hml index 29ff1c52..06fc64d2 100644 --- a/fixtures/hasura/sample_mflix/metadata/models/Movies.hml +++ b/fixtures/hasura/sample_mflix/metadata/models/Movies.hml @@ -143,7 +143,7 @@ definition: - name: fresh type: Int - name: lastUpdated - type: Date! + type: String! - name: production type: String - name: rotten @@ -204,7 +204,7 @@ definition: - name: languages type: "[String!]" - name: lastupdated - type: String! + type: Date! - name: metacritic type: Int - name: numMflixComments