diff --git a/CHANGELOG.md b/CHANGELOG.md index 758d546b..c2517715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,113 @@ This changelog documents the changes between release versions. ### Added - Extended JSON fields now support all comparison and aggregation functions ([#99](https://github.com/hasura/ndc-mongodb/pull/99)) +- Update to ndc-spec v0.1.6 which allows filtering by object values in array fields ([#101](https://github.com/hasura/ndc-mongodb/pull/101)) + +#### Filtering by values in arrays + +In this update you can filter by making comparisons to object values inside +arrays. For example consider a MongoDB database with these three documents: + +```json +{ "institution": "Black Mesa", "staff": [{ "name": "Freeman" }, { "name": "Calhoun" }] } +{ "institution": "Aperture Science", "staff": [{ "name": "GLaDOS" }, { "name": "Chell" }] } +{ "institution": "City 17", "staff": [{ "name": "Alyx" }, { "name": "Freeman" }, { "name": "Breen" }] } +``` + +You can now write a GraphQL query with a `where` clause that checks individual +entries in the `staff` arrays: + +```graphql +query { + institutions(where: { staff: { name: { _eq: "Freeman" } } }) { + institution + } +} +``` + +Which produces the result: + +```json +{ "data": { "institutions": [ + { "institution": "Black Mesa" }, + { "institution": "City 17" } +] } } +``` + +The filter selects documents where **any** element in the array passes the +condition. If you want to select only documents where _every_ array element +passes then negate the comparison on array element values, and also negate the +entire predicate like this: + +```graphql +query EveryElementMustMatch { + institutions( + where: { _not: { staff: { name: { _neq: "Freeman" } } } } + ) { + institution + } +} +``` + +**Note:** It is currently only possible to filter on arrays that contain +objects. Filtering on arrays that contain scalar values or nested arrays will +come later. + +To configure DDN metadata to filter on array fields configure the +`BooleanExpressionType` for the containing document object type to use an +**object** boolean expression type for comparisons on the array field. The +GraphQL Engine will transparently distribute object comparisons over array +elements. For example the above example is configured with this boolean +expression type for documents: + +```yaml +--- +kind: BooleanExpressionType +version: v1 +definition: + name: InstitutionComparisonExp + operand: + object: + type: Institution + comparableFields: + - fieldName: id + booleanExpressionType: ObjectIdComparisonExp + - fieldName: institution + booleanExpressionType: StringComparisonExp + - fieldName: staff + booleanExpressionType: InstitutionStaffComparisonExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: InstitutionComparisonExp +``` + +`InstitutionStaffComparisonExp` is the boolean expression type for objects +inside the `staff` array. It looks like this: + +```yaml +--- +kind: BooleanExpressionType +version: v1 +definition: + name: InstitutionStaffComparisonExp + operand: + object: + type: InstitutionStaff + comparableFields: + - fieldName: name + booleanExpressionType: StringComparisonExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: InstitutionStaffComparisonExp +``` ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 3f1ef987..71a2bdc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,8 +1840,8 @@ dependencies = [ [[package]] name = "ndc-models" -version = "0.1.5" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.5#78f52768bd02a8289194078a5abc2432c8e3a758" +version = "0.1.6" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" dependencies = [ "indexmap 2.2.6", "ref-cast", @@ -1874,8 +1874,8 @@ dependencies = [ [[package]] name = "ndc-sdk" -version = "0.2.1" -source = "git+https://github.com/hasura/ndc-sdk-rs.git?tag=v0.2.1#83a906e8a744ee78d84aeee95f61bf3298a982ea" +version = "0.4.0" +source = "git+https://github.com/hasura/ndc-sdk-rs.git?tag=v0.4.0#665509f7d3b47ce4f014fc23f817a3599ba13933" dependencies = [ "async-trait", "axum", @@ -1907,8 +1907,8 @@ dependencies = [ [[package]] name = "ndc-test" -version = "0.1.5" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.5#78f52768bd02a8289194078a5abc2432c8e3a758" +version = "0.1.6" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" dependencies = [ "async-trait", "clap", diff --git a/Cargo.toml b/Cargo.toml index dc7a9e4b..f03a0430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ resolver = "2" # The tag or rev of ndc-models must match the locked tag or rev of the # ndc-models dependency of ndc-sdk [workspace.dependencies] -ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs.git", tag = "v0.2.1" } -ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.5" } +ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs.git", tag = "v0.4.0" } +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.6" } indexmap = { version = "2", features = [ "serde", diff --git a/arion-compose/e2e-testing.nix b/arion-compose/e2e-testing.nix index 2c2822c2..ee562b1b 100644 --- a/arion-compose/e2e-testing.nix +++ b/arion-compose/e2e-testing.nix @@ -20,7 +20,7 @@ in connector = import ./services/connector.nix { inherit pkgs; - configuration-dir = ../fixtures/hasura/chinook/connector/chinook; + configuration-dir = ../fixtures/hasura/chinook/connector; database-uri = "mongodb://mongodb/chinook"; port = connector-port; service.depends_on.mongodb.condition = "service_healthy"; diff --git a/arion-compose/integration-test-services.nix b/arion-compose/integration-test-services.nix index 1d6b7921..1b7fd813 100644 --- a/arion-compose/integration-test-services.nix +++ b/arion-compose/integration-test-services.nix @@ -12,6 +12,7 @@ , otlp-endpoint ? null , connector-port ? "7130" , connector-chinook-port ? "7131" +, connector-test-cases-port ? "7132" , engine-port ? "7100" , mongodb-port ? "27017" }: @@ -21,7 +22,7 @@ in { connector = import ./services/connector.nix { inherit pkgs otlp-endpoint; - configuration-dir = ../fixtures/hasura/sample_mflix/connector/sample_mflix; + configuration-dir = ../fixtures/hasura/sample_mflix/connector; database-uri = "mongodb://mongodb/sample_mflix"; port = connector-port; hostPort = hostPort connector-port; @@ -32,7 +33,7 @@ in connector-chinook = import ./services/connector.nix { inherit pkgs otlp-endpoint; - configuration-dir = ../fixtures/hasura/chinook/connector/chinook; + configuration-dir = ../fixtures/hasura/chinook/connector; database-uri = "mongodb://mongodb/chinook"; port = connector-chinook-port; hostPort = hostPort connector-chinook-port; @@ -41,6 +42,17 @@ in }; }; + connector-test-cases = import ./services/connector.nix { + inherit pkgs otlp-endpoint; + configuration-dir = ../fixtures/hasura/test_cases/connector; + database-uri = "mongodb://mongodb/test_cases"; + port = connector-test-cases-port; + hostPort = hostPort connector-test-cases-port; + service.depends_on = { + mongodb.condition = "service_healthy"; + }; + }; + mongodb = import ./services/mongodb.nix { inherit pkgs; port = mongodb-port; @@ -60,10 +72,12 @@ in connectors = { chinook = "http://connector-chinook:${connector-chinook-port}"; sample_mflix = "http://connector:${connector-port}"; + test_cases = "http://connector-test-cases:${connector-test-cases-port}"; }; ddn-dirs = [ ../fixtures/hasura/chinook/metadata ../fixtures/hasura/sample_mflix/metadata + ../fixtures/hasura/test_cases/metadata ../fixtures/hasura/common/metadata ]; service.depends_on = { diff --git a/arion-compose/integration-tests.nix b/arion-compose/integration-tests.nix index 6e45df8d..5ef5ec56 100644 --- a/arion-compose/integration-tests.nix +++ b/arion-compose/integration-tests.nix @@ -11,6 +11,7 @@ let connector-port = "7130"; connector-chinook-port = "7131"; + connector-test-cases-port = "7132"; engine-port = "7100"; services = import ./integration-test-services.nix { @@ -26,10 +27,12 @@ in inherit pkgs; connector-url = "http://connector:${connector-port}/"; connector-chinook-url = "http://connector-chinook:${connector-chinook-port}/"; + connector-test-cases-url = "http://connector-test-cases:${connector-test-cases-port}/"; engine-graphql-url = "http://engine:${engine-port}/graphql"; service.depends_on = { connector.condition = "service_healthy"; connector-chinook.condition = "service_healthy"; + connector-test-cases.condition = "service_healthy"; engine.condition = "service_healthy"; }; # Run the container as the current user so when it writes to the snapshots diff --git a/arion-compose/ndc-test.nix b/arion-compose/ndc-test.nix index 4f39e3b7..9af28502 100644 --- a/arion-compose/ndc-test.nix +++ b/arion-compose/ndc-test.nix @@ -14,7 +14,7 @@ in # command = ["test" "--snapshots-dir" "/snapshots" "--seed" "1337_1337_1337_1337_1337_1337_13"]; # Replay and test the recorded snapshots # command = ["replay" "--snapshots-dir" "/snapshots"]; - configuration-dir = ../fixtures/hasura/chinook/connector/chinook; + configuration-dir = ../fixtures/hasura/chinook/connector; database-uri = "mongodb://mongodb:${mongodb-port}/chinook"; service.depends_on.mongodb.condition = "service_healthy"; # Run the container as the current user so when it writes to the snapshots directory it doesn't write as root diff --git a/arion-compose/services/connector.nix b/arion-compose/services/connector.nix index f542619d..abca3c00 100644 --- a/arion-compose/services/connector.nix +++ b/arion-compose/services/connector.nix @@ -12,7 +12,7 @@ , profile ? "dev" # Rust crate profile, usually either "dev" or "release" , hostPort ? null , command ? ["serve"] -, configuration-dir ? ../../fixtures/hasura/sample_mflix/connector/sample_mflix +, configuration-dir ? ../../fixtures/hasura/sample_mflix/connector , database-uri ? "mongodb://mongodb/sample_mflix" , service ? { } # additional options to customize this service configuration , otlp-endpoint ? null @@ -32,16 +32,14 @@ let "${hostPort}:${port}" # host:container ]; environment = pkgs.lib.filterAttrs (_: v: v != null) { - HASURA_CONFIGURATION_DIRECTORY = "/configuration"; + HASURA_CONFIGURATION_DIRECTORY = (pkgs.lib.sources.cleanSource configuration-dir).outPath; HASURA_CONNECTOR_PORT = port; MONGODB_DATABASE_URI = database-uri; OTEL_SERVICE_NAME = "mongodb-connector"; OTEL_EXPORTER_OTLP_ENDPOINT = otlp-endpoint; RUST_LOG = "configuration=debug,mongodb_agent_common=debug,mongodb_connector=debug,mongodb_support=debug,ndc_query_plan=debug"; }; - volumes = [ - "${configuration-dir}:/configuration:ro" - ] ++ extra-volumes; + volumes = extra-volumes; healthcheck = { test = [ "CMD" "${pkgs.pkgsCross.linux.curl}/bin/curl" "-f" "http://localhost:${port}/health" ]; start_period = "5s"; diff --git a/arion-compose/services/integration-tests.nix b/arion-compose/services/integration-tests.nix index e25d3770..00d55c4e 100644 --- a/arion-compose/services/integration-tests.nix +++ b/arion-compose/services/integration-tests.nix @@ -1,6 +1,7 @@ { pkgs , connector-url , connector-chinook-url +, connector-test-cases-url , engine-graphql-url , service ? { } # additional options to customize this service configuration }: @@ -16,6 +17,7 @@ let environment = { CONNECTOR_URL = connector-url; CONNECTOR_CHINOOK_URL = connector-chinook-url; + CONNECTOR_TEST_CASES_URL = connector-test-cases-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/tests/filtering.rs b/crates/integration-tests/src/tests/filtering.rs index e0684d97..18ae718f 100644 --- a/crates/integration-tests/src/tests/filtering.rs +++ b/crates/integration-tests/src/tests/filtering.rs @@ -31,3 +31,24 @@ async fn filters_on_extended_json_using_string_comparison() -> anyhow::Result<() ); Ok(()) } + +#[tokio::test] +async fn filters_by_comparisons_on_elements_of_array_field() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + testCases_nestedCollection( + where: { staff: { name: { _eq: "Freeman" } } } + order_by: { institution: Asc } + ) { + institution + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparisons_on_elements_of_array_field.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparisons_on_elements_of_array_field.snap new file mode 100644 index 00000000..37db004b --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparisons_on_elements_of_array_field.snap @@ -0,0 +1,9 @@ +--- +source: crates/integration-tests/src/tests/filtering.rs +expression: "graphql_query(r#\"\n query {\n testCases_nestedCollection(\n where: { staff: { name: { _eq: \"Freeman\" } } }\n order_by: { institution: Asc }\n ) {\n institution\n }\n }\n \"#).run().await?" +--- +data: + testCases_nestedCollection: + - institution: Black Mesa + - institution: City 17 +errors: ~ diff --git a/crates/mongodb-agent-common/src/health.rs b/crates/mongodb-agent-common/src/health.rs deleted file mode 100644 index fd1d064b..00000000 --- a/crates/mongodb-agent-common/src/health.rs +++ /dev/null @@ -1,15 +0,0 @@ -use http::StatusCode; -use mongodb::bson::{doc, Document}; - -use crate::{interface_types::MongoAgentError, state::ConnectorState}; - -pub async fn check_health(state: &ConnectorState) -> Result { - let db = state.database(); - - let status: Result = db.run_command(doc! { "ping": 1 }, None).await; - - match status { - Ok(_) => Ok(StatusCode::NO_CONTENT), - Err(_) => Ok(StatusCode::SERVICE_UNAVAILABLE), - } -} diff --git a/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs b/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs index a549ec58..97fb6e8e 100644 --- a/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs +++ b/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs @@ -1,4 +1,7 @@ -use std::fmt::{self, Display}; +use std::{ + borrow::Cow, + fmt::{self, Display}, +}; use http::StatusCode; use mongodb::bson; @@ -19,7 +22,7 @@ pub enum MongoAgentError { MongoDBDeserialization(#[from] mongodb::bson::de::Error), MongoDBSerialization(#[from] mongodb::bson::ser::Error), MongoDBSupport(#[from] mongodb_support::error::Error), - NotImplemented(&'static str), + NotImplemented(Cow<'static, str>), Procedure(#[from] ProcedureError), QueryPlan(#[from] QueryPlanError), ResponseSerialization(#[from] QueryResponseError), diff --git a/crates/mongodb-agent-common/src/lib.rs b/crates/mongodb-agent-common/src/lib.rs index 4fcd6596..ff8e8132 100644 --- a/crates/mongodb-agent-common/src/lib.rs +++ b/crates/mongodb-agent-common/src/lib.rs @@ -1,7 +1,6 @@ pub mod aggregation_function; pub mod comparison_function; pub mod explain; -pub mod health; pub mod interface_types; pub mod mongo_query_plan; pub mod mongodb; diff --git a/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs b/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs index 4f378667..a6ed333c 100644 --- a/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs +++ b/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs @@ -11,8 +11,6 @@ use crate::aggregation_function::AggregationFunction; use crate::comparison_function::ComparisonFunction; use crate::scalar_types_capabilities::SCALAR_TYPES; -pub use ndc_query_plan::OrderByTarget; - #[derive(Clone, Debug)] pub struct MongoConfiguration(pub Configuration); @@ -103,7 +101,7 @@ pub type Argument = ndc_query_plan::Argument; pub type Arguments = ndc_query_plan::Arguments; pub type ComparisonTarget = ndc_query_plan::ComparisonTarget; pub type ComparisonValue = ndc_query_plan::ComparisonValue; -pub type ExistsInCollection = ndc_query_plan::ExistsInCollection; +pub type ExistsInCollection = ndc_query_plan::ExistsInCollection; pub type Expression = ndc_query_plan::Expression; pub type Field = ndc_query_plan::Field; pub type MutationOperation = ndc_query_plan::MutationOperation; @@ -114,6 +112,7 @@ pub type NestedArray = ndc_query_plan::NestedArray; pub type NestedObject = ndc_query_plan::NestedObject; pub type ObjectType = ndc_query_plan::ObjectType; pub type OrderBy = ndc_query_plan::OrderBy; +pub type OrderByTarget = ndc_query_plan::OrderByTarget; pub type Query = ndc_query_plan::Query; pub type QueryPlan = ndc_query_plan::QueryPlan; pub type Relationship = ndc_query_plan::Relationship; diff --git a/crates/mongodb-agent-common/src/query/column_ref.rs b/crates/mongodb-agent-common/src/query/column_ref.rs index cd0bef69..9baf31a7 100644 --- a/crates/mongodb-agent-common/src/query/column_ref.rs +++ b/crates/mongodb-agent-common/src/query/column_ref.rs @@ -1,9 +1,17 @@ +// Some of the methods here have been added to support future work - suppressing the dead code +// check prevents warnings in the meantime. +#![allow(dead_code)] + use std::{borrow::Cow, iter::once}; use mongodb::bson::{doc, Bson}; use ndc_query_plan::Scope; -use crate::{mongo_query_plan::ComparisonTarget, mongodb::sanitize::is_name_safe}; +use crate::{ + interface_types::MongoAgentError, + mongo_query_plan::{ComparisonTarget, OrderByTarget}, + mongodb::sanitize::is_name_safe, +}; /// Reference to a document field, or a nested property of a document field. There are two contexts /// where we reference columns: @@ -36,16 +44,56 @@ impl<'a> ColumnRef<'a> { /// If the given target cannot be represented as a match query key, falls back to providing an /// aggregation expression referencing the column. pub fn from_comparison_target(column: &ComparisonTarget) -> ColumnRef<'_> { - from_target(column) + from_comparison_target(column) + } + + /// TODO: This will hopefully become infallible once MDB-150 & MDB-151 are implemented. + pub fn from_order_by_target(target: &OrderByTarget) -> Result, MongoAgentError> { + from_order_by_target(target) + } + + pub fn from_field_path<'b>( + field_path: impl IntoIterator, + ) -> ColumnRef<'b> { + from_path( + None, + field_path + .into_iter() + .map(|field_name| field_name.as_ref() as &str), + ) + .unwrap() + } + + pub fn from_field(field_name: &ndc_models::FieldName) -> ColumnRef<'_> { + fold_path_element(None, field_name.as_ref()) + } + + pub fn into_nested_field<'b: 'a>(self, field_name: &'b ndc_models::FieldName) -> ColumnRef<'b> { + fold_path_element(Some(self), field_name.as_ref()) + } + + pub fn into_aggregate_expression(self) -> Bson { + match self { + ColumnRef::MatchKey(key) => format!("${key}").into(), + ColumnRef::Expression(expr) => expr, + } } } -fn from_target(column: &ComparisonTarget) -> ColumnRef<'_> { +fn from_comparison_target(column: &ComparisonTarget) -> ColumnRef<'_> { match column { + // We exclude `path` (the relationship path) from the resulting ColumnRef because MongoDB + // field references are not relationship-aware. Traversing relationship references is + // handled upstream. ComparisonTarget::Column { name, field_path, .. } => { - let name_and_path = once(name).chain(field_path.iter().flatten()); + let name_and_path = once(name.as_ref() as &str).chain( + field_path + .iter() + .flatten() + .map(|field_name| field_name.as_ref() as &str), + ); // The None case won't come up if the input to [from_target_helper] has at least // one element, and we know it does because we start the iterable with `name` from_path(None, name_and_path).unwrap() @@ -62,8 +110,16 @@ fn from_target(column: &ComparisonTarget) -> ColumnRef<'_> { let init = ColumnRef::MatchKey(format!("${}", name_from_scope(scope)).into()); // The None case won't come up if the input to [from_target_helper] has at least // one element, and we know it does because we start the iterable with `name` - let col_ref = - from_path(Some(init), once(name).chain(field_path.iter().flatten())).unwrap(); + let col_ref = from_path( + Some(init), + once(name.as_ref() as &str).chain( + field_path + .iter() + .flatten() + .map(|field_name| field_name.as_ref() as &str), + ), + ) + .unwrap(); match col_ref { // move from MatchKey to Expression because "$$ROOT" is not valid in a match key ColumnRef::MatchKey(key) => ColumnRef::Expression(format!("${key}").into()), @@ -73,6 +129,39 @@ fn from_target(column: &ComparisonTarget) -> ColumnRef<'_> { } } +fn from_order_by_target(target: &OrderByTarget) -> Result, MongoAgentError> { + match target { + // We exclude `path` (the relationship path) from the resulting ColumnRef because MongoDB + // field references are not relationship-aware. Traversing relationship references is + // handled upstream. + OrderByTarget::Column { + name, field_path, .. + } => { + let name_and_path = once(name.as_ref() as &str).chain( + field_path + .iter() + .flatten() + .map(|field_name| field_name.as_ref() as &str), + ); + // The None case won't come up if the input to [from_target_helper] has at least + // one element, and we know it does because we start the iterable with `name` + Ok(from_path(None, name_and_path).unwrap()) + } + OrderByTarget::SingleColumnAggregate { .. } => { + // TODO: MDB-150 + Err(MongoAgentError::NotImplemented( + "ordering by single column aggregate".into(), + )) + } + OrderByTarget::StarCountAggregate { .. } => { + // TODO: MDB-151 + Err(MongoAgentError::NotImplemented( + "ordering by star count aggregate".into(), + )) + } + } +} + pub fn name_from_scope(scope: &Scope) -> Cow<'_, str> { match scope { Scope::Root => "scope_root".into(), @@ -82,10 +171,10 @@ pub fn name_from_scope(scope: &Scope) -> Cow<'_, str> { fn from_path<'a>( init: Option>, - path: impl IntoIterator, + path: impl IntoIterator, ) -> Option> { path.into_iter().fold(init, |accum, element| { - Some(fold_path_element(accum, element.as_ref())) + Some(fold_path_element(accum, element)) }) } @@ -140,10 +229,7 @@ fn fold_path_element<'a>( /// Unlike `column_ref` this expression cannot be used as a match query key - it can only be used /// as an expression. pub fn column_expression(column: &ComparisonTarget) -> Bson { - match ColumnRef::from_comparison_target(column) { - ColumnRef::MatchKey(key) => format!("${key}").into(), - ColumnRef::Expression(expr) => expr, - } + ColumnRef::from_comparison_target(column).into_aggregate_expression() } #[cfg(test)] diff --git a/crates/mongodb-agent-common/src/query/execute_query_request.rs b/crates/mongodb-agent-common/src/query/execute_query_request.rs index bf107318..d1193ebc 100644 --- a/crates/mongodb-agent-common/src/query/execute_query_request.rs +++ b/crates/mongodb-agent-common/src/query/execute_query_request.rs @@ -24,7 +24,12 @@ pub async fn execute_query_request( config: &MongoConfiguration, query_request: QueryRequest, ) -> Result { + tracing::debug!( + query_request = %serde_json::to_string(&query_request).unwrap(), + "query request" + ); let query_plan = preprocess_query_request(config, query_request)?; + tracing::debug!(?query_plan, "abstract query plan"); let pipeline = pipeline_for_query_request(config, &query_plan)?; let documents = execute_query_pipeline(database, config, &query_plan, pipeline).await?; let response = serialize_query_response(config.extended_json_mode(), &query_plan, documents)?; diff --git a/crates/mongodb-agent-common/src/query/make_selector.rs b/crates/mongodb-agent-common/src/query/make_selector.rs index f7ddb7da..0139ccec 100644 --- a/crates/mongodb-agent-common/src/query/make_selector.rs +++ b/crates/mongodb-agent-common/src/query/make_selector.rs @@ -1,3 +1,5 @@ +use std::iter::once; + use anyhow::anyhow; use mongodb::bson::{self, doc, Document}; use ndc_models::UnaryComparisonOperator; @@ -19,6 +21,34 @@ fn bson_from_scalar_value(value: &serde_json::Value, value_type: &Type) -> Resul json_to_bson(value_type, value.clone()).map_err(|e| MongoAgentError::BadQuery(anyhow!(e))) } +/// Creates a "query document" that filters documents according to the given expression. Query +/// documents are used as arguments for the `$match` aggregation stage, and for the db.find() +/// command. +/// +/// Query documents are distinct from "aggregation expressions". The latter are more general. +/// +/// TODO: NDC-436 To handle complex expressions with sub-expressions that require a switch to an +/// aggregation expression context we need to turn this into multiple functions to handle context +/// switching. Something like this: +/// +/// struct QueryDocument(bson::Document); +/// struct AggregationExpression(bson::Document); +/// +/// enum ExpressionPlan { +/// QueryDocument(QueryDocument), +/// AggregationExpression(AggregationExpression), +/// } +/// +/// fn make_query_document(expr: &Expression) -> QueryDocument; +/// fn make_aggregate_expression(expr: &Expression) -> AggregationExpression; +/// fn make_expression_plan(exr: &Expression) -> ExpressionPlan; +/// +/// The idea is to change `make_selector` to `make_query_document`, and instead of making recursive +/// calls to itself `make_query_document` would make calls to `make_expression_plan` (which would +/// call itself recursively). If any part of the expression plan evaluates to +/// `ExpressionPlan::AggregationExpression(_)` then the entire plan needs to be an aggregation +/// expression, wrapped with the `$expr` query document operator at the top level. So recursion +/// needs to be depth-first. pub fn make_selector(expr: &Expression) -> Result { match expr { Expression::And { expressions } => { @@ -48,6 +78,8 @@ pub fn make_selector(expr: &Expression) -> Result { }, None => doc! { format!("{relationship}.0"): { "$exists": true } }, }, + // TODO: NDC-434 If a `predicate` is not `None` it should be applied to the unrelated + // collection ExistsInCollection::Unrelated { unrelated_collection, } => doc! { @@ -55,6 +87,54 @@ pub fn make_selector(expr: &Expression) -> Result { "$ne": [format!("$$ROOT.{unrelated_collection}.0"), null] } }, + ExistsInCollection::NestedCollection { + column_name, + field_path, + .. + } => { + let column_ref = + ColumnRef::from_field_path(field_path.iter().chain(once(column_name))); + match (column_ref, predicate) { + (ColumnRef::MatchKey(key), Some(predicate)) => doc! { + key: { + "$elemMatch": make_selector(predicate)? + } + }, + (ColumnRef::MatchKey(key), None) => doc! { + key: { + "$exists": true, + "$not": { "$size": 0 }, + } + }, + (ColumnRef::Expression(column_expr), Some(predicate)) => { + // TODO: NDC-436 We need to be able to create a plan for `predicate` that + // evaluates with the variable `$$this` as document root since that + // references each array element. With reference to the plan in the + // TODO comment above, this scoped predicate plan needs to be created + // with `make_aggregate_expression` since we are in an aggregate + // expression context at this point. + let predicate_scoped_to_nested_document: Document = + Err(MongoAgentError::NotImplemented(format!("currently evaluating the predicate, {predicate:?}, in a nested collection context is not implemented").into()))?; + doc! { + "$expr": { + "$anyElementTrue": { + "$map": { + "input": column_expr, + "in": predicate_scoped_to_nested_document, + } + } + } + } + } + (ColumnRef::Expression(column_expr), None) => { + doc! { + "$expr": { + "$gt": [{ "$size": column_expr }, 0] + } + } + } + } + } }), Expression::BinaryComparisonOperator { column, @@ -95,7 +175,7 @@ fn make_binary_comparison_selector( || !value_column.relationship_path().is_empty() { return Err(MongoAgentError::NotImplemented( - "binary comparisons between two fields where either field is in a related collection", + "binary comparisons between two fields where either field is in a related collection".into(), )); } doc! { @@ -174,7 +254,9 @@ mod tests { use crate::{ comparison_function::ComparisonFunction, - mongo_query_plan::{ComparisonTarget, ComparisonValue, Expression, Type}, + mongo_query_plan::{ + ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Type, + }, query::pipeline_for_query_request, test_helpers::{chinook_config, chinook_relationships}, }; @@ -386,4 +468,74 @@ mod tests { assert_eq!(bson::to_bson(&pipeline).unwrap(), expected_pipeline); Ok(()) } + + #[test] + fn compares_value_to_elements_of_array_field() -> anyhow::Result<()> { + let selector = make_selector(&Expression::Exists { + in_collection: ExistsInCollection::NestedCollection { + column_name: "staff".into(), + arguments: Default::default(), + field_path: Default::default(), + }, + predicate: Some(Box::new(Expression::BinaryComparisonOperator { + column: ComparisonTarget::Column { + name: "last_name".into(), + field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + field_path: Default::default(), + path: Default::default(), + }, + operator: ComparisonFunction::Equal, + value: ComparisonValue::Scalar { + value: "Hughes".into(), + value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + }, + })), + })?; + + let expected = doc! { + "staff": { + "$elemMatch": { + "last_name": { "$eq": "Hughes" } + } + } + }; + + assert_eq!(selector, expected); + Ok(()) + } + + #[test] + fn compares_value_to_elements_of_array_field_of_nested_object() -> anyhow::Result<()> { + let selector = make_selector(&Expression::Exists { + in_collection: ExistsInCollection::NestedCollection { + column_name: "staff".into(), + arguments: Default::default(), + field_path: vec!["site_info".into()], + }, + predicate: Some(Box::new(Expression::BinaryComparisonOperator { + column: ComparisonTarget::Column { + name: "last_name".into(), + field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + field_path: Default::default(), + path: Default::default(), + }, + operator: ComparisonFunction::Equal, + value: ComparisonValue::Scalar { + value: "Hughes".into(), + value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + }, + })), + })?; + + let expected = doc! { + "site_info.staff": { + "$elemMatch": { + "last_name": { "$eq": "Hughes" } + } + } + }; + + assert_eq!(selector, expected); + Ok(()) + } } diff --git a/crates/mongodb-agent-common/src/query/make_sort.rs b/crates/mongodb-agent-common/src/query/make_sort.rs index e113da4e..ead5ceb4 100644 --- a/crates/mongodb-agent-common/src/query/make_sort.rs +++ b/crates/mongodb-agent-common/src/query/make_sort.rs @@ -37,12 +37,12 @@ pub fn make_sort(order_by: &OrderBy) -> Result { // TODO: MDB-150 { Err(MongoAgentError::NotImplemented( - "ordering by single column aggregate", + "ordering by single column aggregate".into(), )) } OrderByTarget::StarCountAggregate { path: _ } => Err( // TODO: MDB-151 - MongoAgentError::NotImplemented("ordering by star count aggregate"), + MongoAgentError::NotImplemented("ordering by star count aggregate".into()), ), } }) diff --git a/crates/mongodb-connector/src/capabilities.rs b/crates/mongodb-connector/src/capabilities.rs index 460be3cd..0d71a91e 100644 --- a/crates/mongodb-connector/src/capabilities.rs +++ b/crates/mongodb-connector/src/capabilities.rs @@ -1,5 +1,5 @@ use ndc_sdk::models::{ - Capabilities, LeafCapability, NestedFieldCapabilities, QueryCapabilities, + Capabilities, ExistsCapabilities, LeafCapability, NestedFieldCapabilities, QueryCapabilities, RelationshipCapabilities, }; @@ -14,6 +14,9 @@ pub fn mongo_capabilities() -> Capabilities { order_by: Some(LeafCapability {}), aggregates: None, }, + exists: ExistsCapabilities { + nested_collections: Some(LeafCapability {}), + }, }, mutation: ndc_sdk::models::MutationCapabilities { transactional: None, diff --git a/crates/mongodb-connector/src/error_mapping.rs b/crates/mongodb-connector/src/error_mapping.rs deleted file mode 100644 index 6db47afc..00000000 --- a/crates/mongodb-connector/src/error_mapping.rs +++ /dev/null @@ -1,43 +0,0 @@ -use http::StatusCode; -use mongodb_agent_common::interface_types::{ErrorResponse, MongoAgentError}; -use ndc_sdk::{ - connector::{ExplainError, QueryError}, - models, -}; -use serde_json::Value; - -pub fn mongo_agent_error_to_query_error(error: MongoAgentError) -> QueryError { - if let MongoAgentError::NotImplemented(e) = error { - return QueryError::UnsupportedOperation(error_response(e.to_owned())); - } - let (status, err) = error.status_and_error_response(); - match status { - StatusCode::BAD_REQUEST => QueryError::UnprocessableContent(convert_error_response(err)), - _ => QueryError::Other(Box::new(error), Value::Object(Default::default())), - } -} - -pub fn mongo_agent_error_to_explain_error(error: MongoAgentError) -> ExplainError { - if let MongoAgentError::NotImplemented(e) = error { - return ExplainError::UnsupportedOperation(error_response(e.to_owned())); - } - let (status, err) = error.status_and_error_response(); - match status { - StatusCode::BAD_REQUEST => ExplainError::UnprocessableContent(convert_error_response(err)), - _ => ExplainError::Other(Box::new(error), Value::Object(Default::default())), - } -} - -pub fn error_response(message: String) -> models::ErrorResponse { - models::ErrorResponse { - message, - details: serde_json::Value::Object(Default::default()), - } -} - -pub fn convert_error_response(err: ErrorResponse) -> models::ErrorResponse { - models::ErrorResponse { - message: err.message, - details: Value::Object(err.details.unwrap_or_default().into_iter().collect()), - } -} diff --git a/crates/mongodb-connector/src/main.rs b/crates/mongodb-connector/src/main.rs index abcab866..bc9ed2a9 100644 --- a/crates/mongodb-connector/src/main.rs +++ b/crates/mongodb-connector/src/main.rs @@ -1,14 +1,11 @@ mod capabilities; -mod error_mapping; mod mongo_connector; mod mutation; mod schema; -use std::error::Error; - use mongo_connector::MongoConnector; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> ndc_sdk::connector::Result<()> { ndc_sdk::default_main::default_main::().await } diff --git a/crates/mongodb-connector/src/mongo_connector.rs b/crates/mongodb-connector/src/mongo_connector.rs index 5df795a3..538913af 100644 --- a/crates/mongodb-connector/src/mongo_connector.rs +++ b/crates/mongodb-connector/src/mongo_connector.rs @@ -1,29 +1,23 @@ use std::path::Path; -use anyhow::anyhow; use async_trait::async_trait; use configuration::Configuration; +use http::StatusCode; use mongodb_agent_common::{ - explain::explain_query, health::check_health, mongo_query_plan::MongoConfiguration, + explain::explain_query, interface_types::MongoAgentError, mongo_query_plan::MongoConfiguration, query::handle_query_request, state::ConnectorState, }; use ndc_sdk::{ - connector::{ - Connector, ConnectorSetup, ExplainError, FetchMetricsError, HealthError, - InitializationError, MutationError, ParseError, QueryError, SchemaError, - }, + connector::{self, Connector, ConnectorSetup, ErrorResponse}, json_response::JsonResponse, models::{ Capabilities, ExplainResponse, MutationRequest, MutationResponse, QueryRequest, QueryResponse, SchemaResponse, }, }; -use serde_json::Value; +use serde_json::json; use tracing::instrument; -use crate::error_mapping::{ - error_response, mongo_agent_error_to_explain_error, mongo_agent_error_to_query_error, -}; use crate::{capabilities::mongo_capabilities, mutation::handle_mutation_request}; #[derive(Clone, Default)] @@ -38,10 +32,16 @@ impl ConnectorSetup for MongoConnector { async fn parse_configuration( &self, configuration_dir: impl AsRef + Send, - ) -> Result { + ) -> connector::Result { let configuration = Configuration::parse_configuration(configuration_dir) .await - .map_err(|err| ParseError::Other(err.into()))?; + .map_err(|err| { + ErrorResponse::new( + StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + json!({}), + ) + })?; Ok(MongoConfiguration(configuration)) } @@ -54,7 +54,7 @@ impl ConnectorSetup for MongoConnector { &self, _configuration: &MongoConfiguration, _metrics: &mut prometheus::Registry, - ) -> Result { + ) -> connector::Result { let state = mongodb_agent_common::state::try_init_state().await?; Ok(state) } @@ -70,27 +70,10 @@ impl Connector for MongoConnector { fn fetch_metrics( _configuration: &Self::Configuration, _state: &Self::State, - ) -> Result<(), FetchMetricsError> { + ) -> connector::Result<()> { Ok(()) } - #[instrument(err, skip_all)] - async fn health_check( - _configuration: &Self::Configuration, - state: &Self::State, - ) -> Result<(), HealthError> { - let status = check_health(state) - .await - .map_err(|e| HealthError::Other(e.into(), Value::Object(Default::default())))?; - match status.as_u16() { - 200..=299 => Ok(()), - s => Err(HealthError::Other( - anyhow!("unhealthy status: {s}").into(), - Value::Object(Default::default()), - )), - } - } - async fn get_capabilities() -> Capabilities { mongo_capabilities() } @@ -98,7 +81,7 @@ impl Connector for MongoConnector { #[instrument(err, skip_all)] async fn get_schema( configuration: &Self::Configuration, - ) -> Result, SchemaError> { + ) -> connector::Result> { let response = crate::schema::get_schema(configuration).await?; Ok(response.into()) } @@ -108,10 +91,10 @@ impl Connector for MongoConnector { configuration: &Self::Configuration, state: &Self::State, request: QueryRequest, - ) -> Result, ExplainError> { + ) -> connector::Result> { let response = explain_query(configuration, state, request) .await - .map_err(mongo_agent_error_to_explain_error)?; + .map_err(map_mongo_agent_error)?; Ok(response.into()) } @@ -120,10 +103,12 @@ impl Connector for MongoConnector { _configuration: &Self::Configuration, _state: &Self::State, _request: MutationRequest, - ) -> Result, ExplainError> { - Err(ExplainError::UnsupportedOperation(error_response( - "Explain for mutations is not implemented yet".to_owned(), - ))) + ) -> connector::Result> { + Err(ErrorResponse::new( + StatusCode::NOT_IMPLEMENTED, + "Explain for mutations is not implemented yet".to_string(), + json!({}), + )) } #[instrument(err, skip_all)] @@ -131,8 +116,9 @@ impl Connector for MongoConnector { configuration: &Self::Configuration, state: &Self::State, request: MutationRequest, - ) -> Result, MutationError> { - handle_mutation_request(configuration, state, request).await + ) -> connector::Result> { + let response = handle_mutation_request(configuration, state, request).await?; + Ok(response) } #[instrument(name = "/query", err, skip_all, fields(internal.visibility = "user"))] @@ -140,10 +126,19 @@ impl Connector for MongoConnector { configuration: &Self::Configuration, state: &Self::State, request: QueryRequest, - ) -> Result, QueryError> { + ) -> connector::Result> { let response = handle_query_request(configuration, state, request) .await - .map_err(mongo_agent_error_to_query_error)?; + .map_err(map_mongo_agent_error)?; Ok(response.into()) } } + +fn map_mongo_agent_error(err: MongoAgentError) -> ErrorResponse { + let (status_code, err_response) = err.status_and_error_response(); + let details = match err_response.details { + Some(details) => details.into_iter().collect(), + None => json!({}), + }; + ErrorResponse::new(status_code, err_response.message, details) +} diff --git a/crates/mongodb-connector/src/mutation.rs b/crates/mongodb-connector/src/mutation.rs index e517dbb4..7b932fbd 100644 --- a/crates/mongodb-connector/src/mutation.rs +++ b/crates/mongodb-connector/src/mutation.rs @@ -17,10 +17,9 @@ use ndc_query_plan::plan_for_mutation_request; use ndc_sdk::{ connector::MutationError, json_response::JsonResponse, - models::{MutationOperationResults, MutationRequest, MutationResponse}, + models::{ErrorResponse, MutationOperationResults, MutationRequest, MutationResponse}, }; - -use crate::error_mapping::error_response; +use serde_json::json; pub async fn handle_mutation_request( config: &MongoConfiguration, @@ -29,10 +28,10 @@ pub async fn handle_mutation_request( ) -> Result, MutationError> { tracing::debug!(?config, mutation_request = %serde_json::to_string(&mutation_request).unwrap(), "executing mutation"); let mutation_plan = plan_for_mutation_request(config, mutation_request).map_err(|err| { - MutationError::UnprocessableContent(error_response(format!( - "error processing mutation request: {}", - err - ))) + MutationError::UnprocessableContent(ErrorResponse { + message: format!("error processing mutation request: {}", err), + details: json!({}), + }) })?; let database = state.database(); let jobs = look_up_procedures(config, &mutation_plan)?; @@ -71,12 +70,13 @@ fn look_up_procedures<'a, 'b>( .partition_result(); if !not_found.is_empty() { - return Err(MutationError::UnprocessableContent(error_response( - format!( + return Err(MutationError::UnprocessableContent(ErrorResponse { + message: format!( "request includes unknown mutations: {}", not_found.join(", ") ), - ))); + details: json!({}), + })); } Ok(procedures) @@ -88,16 +88,22 @@ async fn execute_procedure( procedure: Procedure<'_>, requested_fields: Option<&NestedField>, ) -> Result { - let (result, result_type) = procedure - .execute(database.clone()) - .await - .map_err(|err| MutationError::UnprocessableContent(error_response(err.to_string())))?; + let (result, result_type) = procedure.execute(database.clone()).await.map_err(|err| { + MutationError::UnprocessableContent(ErrorResponse { + message: err.to_string(), + details: json!({}), + }) + })?; let rewritten_result = rewrite_response(requested_fields, result.into())?; let requested_result_type = if let Some(fields) = requested_fields { - type_for_nested_field(&[], &result_type, fields) - .map_err(|err| MutationError::UnprocessableContent(error_response(err.to_string())))? + type_for_nested_field(&[], &result_type, fields).map_err(|err| { + MutationError::UnprocessableContent(ErrorResponse { + message: err.to_string(), + details: json!({}), + }) + })? } else { result_type }; @@ -107,7 +113,12 @@ async fn execute_procedure( &requested_result_type, rewritten_result, ) - .map_err(|err| MutationError::UnprocessableContent(error_response(err.to_string())))?; + .map_err(|err| { + MutationError::UnprocessableContent(ErrorResponse { + message: err.to_string(), + details: json!({}), + }) + })?; Ok(MutationOperationResults::Procedure { result: json_result, @@ -130,12 +141,18 @@ fn rewrite_response( Ok(rewrite_array(fields, values)?.into()) } - (Some(NestedField::Object(_)), _) => Err(MutationError::UnprocessableContent( - error_response("expected an object".to_owned()), - )), - (Some(NestedField::Array(_)), _) => Err(MutationError::UnprocessableContent( - error_response("expected an array".to_owned()), - )), + (Some(NestedField::Object(_)), _) => { + Err(MutationError::UnprocessableContent(ErrorResponse { + message: "expected an object".to_owned(), + details: json!({}), + })) + } + (Some(NestedField::Array(_)), _) => { + Err(MutationError::UnprocessableContent(ErrorResponse { + message: "expected an array".to_owned(), + details: json!({}), + })) + } } } @@ -154,15 +171,18 @@ fn rewrite_doc( fields, } => { let orig_value = doc.remove(column.as_str()).ok_or_else(|| { - MutationError::UnprocessableContent(error_response(format!( - "missing expected field from response: {name}" - ))) + MutationError::UnprocessableContent(ErrorResponse { + message: format!("missing expected field from response: {name}"), + details: json!({}), + }) })?; rewrite_response(fields.as_ref(), orig_value) } Field::Relationship { .. } => Err(MutationError::UnsupportedOperation( - error_response("The MongoDB connector does not support relationship references in mutations" - .to_owned()), + ErrorResponse { + message: "The MongoDB connector does not support relationship references in mutations".to_owned(), + details: json!({}), + }, )), }?; diff --git a/crates/mongodb-connector/src/schema.rs b/crates/mongodb-connector/src/schema.rs index d24c8d5e..1e92d403 100644 --- a/crates/mongodb-connector/src/schema.rs +++ b/crates/mongodb-connector/src/schema.rs @@ -2,10 +2,10 @@ use mongodb_agent_common::{ mongo_query_plan::MongoConfiguration, scalar_types_capabilities::SCALAR_TYPES, }; use ndc_query_plan::QueryContext as _; -use ndc_sdk::{connector::SchemaError, models as ndc}; +use ndc_sdk::{connector, models as ndc}; -pub async fn get_schema(config: &MongoConfiguration) -> Result { - Ok(ndc::SchemaResponse { +pub async fn get_schema(config: &MongoConfiguration) -> connector::Result { + let schema = ndc::SchemaResponse { collections: config.collections().values().cloned().collect(), functions: config .functions() @@ -20,5 +20,7 @@ pub async fn get_schema(config: &MongoConfiguration) -> Result( } } +/// Given the type of a collection and a field path returns the object type of the nested object at +/// that path. +pub fn find_nested_collection_type( + collection_object_type: plan::ObjectType, + field_path: &[ndc::FieldName], +) -> Result> +where + S: Clone, +{ + fn normalize_object_type( + field_path: &[ndc::FieldName], + t: plan::Type, + ) -> Result> { + match t { + plan::Type::Object(t) => Ok(t), + plan::Type::ArrayOf(t) => normalize_object_type(field_path, *t), + plan::Type::Nullable(t) => normalize_object_type(field_path, *t), + _ => Err(QueryPlanError::ExpectedObject { + path: field_path.iter().map(|f| f.to_string()).collect(), + }), + } + } + + field_path + .iter() + .try_fold(collection_object_type, |obj_type, field_name| { + let field_type = find_object_field(&obj_type, field_name)?.clone(); + normalize_object_type(field_path, field_type) + }) +} + pub fn lookup_relationship<'a>( relationships: &'a BTreeMap, relationship: &ndc::RelationshipName, diff --git a/crates/ndc-query-plan/src/plan_for_query_request/mod.rs b/crates/ndc-query-plan/src/plan_for_query_request/mod.rs index 4da4fb04..6e2f7395 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/mod.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/mod.rs @@ -12,15 +12,17 @@ mod plan_test_helpers; #[cfg(test)] mod tests; -use std::collections::VecDeque; +use std::{collections::VecDeque, iter::once}; use crate::{self as plan, type_annotated_field, ObjectType, QueryPlan, Scope}; +use helpers::find_nested_collection_type; use indexmap::IndexMap; use itertools::Itertools; use ndc::{ExistsInCollection, QueryRequest}; use ndc_models as ndc; use query_plan_state::QueryPlanInfo; +pub use self::plan_for_mutation_request::plan_for_mutation_request; use self::{ helpers::{find_object_field, find_object_field_path, lookup_relationship}, plan_for_arguments::plan_for_arguments, @@ -28,7 +30,6 @@ use self::{ query_plan_error::QueryPlanError, query_plan_state::QueryPlanState, }; -pub use self::plan_for_mutation_request::plan_for_mutation_request; type Result = std::result::Result; @@ -698,6 +699,52 @@ fn plan_for_exists( }; Ok((in_collection, predicate)) } + ndc::ExistsInCollection::NestedCollection { + column_name, + arguments, + field_path, + } => { + let arguments = if arguments.is_empty() { + Default::default() + } else { + Err(QueryPlanError::NotImplemented( + "arguments on nested fields".to_string(), + ))? + }; + + // To support field arguments here we need a way to look up field parameters (a map of + // supported argument names to types). When we have that replace the above `arguments` + // assignment with this one: + // let arguments = plan_for_arguments(plan_state, parameters, arguments)?; + + let nested_collection_type = find_nested_collection_type( + root_collection_object_type.clone(), + &field_path + .clone() + .into_iter() + .chain(once(column_name.clone())) + .collect_vec(), + )?; + + let in_collection = plan::ExistsInCollection::NestedCollection { + column_name, + arguments, + field_path, + }; + + let predicate = predicate + .map(|expression| { + plan_for_expression( + &mut nested_state, + root_collection_object_type, + &nested_collection_type, + *expression, + ) + }) + .transpose()?; + + Ok((in_collection, predicate)) + } }?; Ok(plan::Expression::Exists { diff --git a/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs b/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs index e0d0ffc0..4467f802 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs @@ -26,6 +26,9 @@ pub enum QueryPlanError { #[error("missing arguments: {}", .0.join(", "))] MissingArguments(Vec), + #[error("not implemented: {}", .0)] + NotImplemented(String), + #[error("{0}")] RelationshipUnification(#[from] RelationshipUnificationError), diff --git a/crates/ndc-query-plan/src/query_plan.rs b/crates/ndc-query-plan/src/query_plan.rs index 378e8e09..c1a2bafa 100644 --- a/crates/ndc-query-plan/src/query_plan.rs +++ b/crates/ndc-query-plan/src/query_plan.rs @@ -246,7 +246,7 @@ pub enum Expression { value: ComparisonValue, }, Exists { - in_collection: ExistsInCollection, + in_collection: ExistsInCollection, predicate: Option>>, }, } @@ -444,7 +444,7 @@ pub enum ComparisonOperatorDefinition { #[derive(Derivative)] #[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum ExistsInCollection { +pub enum ExistsInCollection { Related { /// Key of the relation in the [Query] joins map. Relationships are scoped to the sub-query /// that defines the relation source. @@ -455,4 +455,10 @@ pub enum ExistsInCollection { /// to a sub-query, instead they are given in the root [QueryPlan]. unrelated_collection: String, }, + NestedCollection { + column_name: ndc::FieldName, + arguments: BTreeMap>, + /// Path to a nested collection via object columns + field_path: Vec, + }, } diff --git a/fixtures/hasura/README.md b/fixtures/hasura/README.md index 4b95bb9b..45f5b3f8 100644 --- a/fixtures/hasura/README.md +++ b/fixtures/hasura/README.md @@ -16,12 +16,27 @@ arion up -d We have two subgraphs, and two connector configurations. So a lot of these commands are repeated for each subgraph + connector combination. -Run introspection to update connector configuration: +Run introspection to update connector configuration. To do that through the ddn +CLI run these commands in the same directory as this README file: ```sh -$ ddn connector introspect --connector sample_mflix/connector/sample_mflix/connector.yaml +$ ddn connector introspect --connector sample_mflix/connector/connector.yaml -$ ddn connector introspect --connector chinook/connector/chinook/connector.yaml +$ ddn connector introspect --connector chinook/connector/connector.yaml + +$ ddn connector introspect --connector test_cases/connector/connector.yaml +``` + +Alternatively run `mongodb-cli-plugin` directly to use the CLI plugin version in +this repo. The plugin binary is provided by the Nix dev shell. Use these +commands: + +```sh +$ mongodb-cli-plugin --connection-uri mongodb://localhost/sample_mflix --context-path sample_mflix/connector/ update + +$ mongodb-cli-plugin --connection-uri mongodb://localhost/chinook --context-path chinook/connector/ update + +$ mongodb-cli-plugin --connection-uri mongodb://localhost/test_cases --context-path test_cases/connector/ update ``` Update Hasura metadata based on connector configuration @@ -32,4 +47,6 @@ introspection): $ ddn connector-link update sample_mflix --subgraph sample_mflix/subgraph.yaml --env-file sample_mflix/.env.sample_mflix --add-all-resources $ ddn connector-link update chinook --subgraph chinook/subgraph.yaml --env-file chinook/.env.chinook --add-all-resources + +$ ddn connector-link update test_cases --subgraph test_cases/subgraph.yaml --env-file test_cases/.env.test_cases --add-all-resources ``` diff --git a/fixtures/hasura/chinook/connector/chinook/.configuration_metadata b/fixtures/hasura/chinook/connector/.configuration_metadata similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/.configuration_metadata rename to fixtures/hasura/chinook/connector/.configuration_metadata diff --git a/fixtures/hasura/chinook/connector/chinook/.ddnignore b/fixtures/hasura/chinook/connector/.ddnignore similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/.ddnignore rename to fixtures/hasura/chinook/connector/.ddnignore diff --git a/fixtures/hasura/chinook/connector/chinook/.env b/fixtures/hasura/chinook/connector/.env similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/.env rename to fixtures/hasura/chinook/connector/.env diff --git a/fixtures/hasura/chinook/connector/chinook/configuration.json b/fixtures/hasura/chinook/connector/configuration.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/configuration.json rename to fixtures/hasura/chinook/connector/configuration.json diff --git a/fixtures/hasura/chinook/connector/chinook/connector.yaml b/fixtures/hasura/chinook/connector/connector.yaml similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/connector.yaml rename to fixtures/hasura/chinook/connector/connector.yaml diff --git a/fixtures/hasura/chinook/connector/chinook/native_mutations/insert_artist.json b/fixtures/hasura/chinook/connector/native_mutations/insert_artist.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/native_mutations/insert_artist.json rename to fixtures/hasura/chinook/connector/native_mutations/insert_artist.json diff --git a/fixtures/hasura/chinook/connector/chinook/native_mutations/update_track_prices.json b/fixtures/hasura/chinook/connector/native_mutations/update_track_prices.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/native_mutations/update_track_prices.json rename to fixtures/hasura/chinook/connector/native_mutations/update_track_prices.json diff --git a/fixtures/hasura/chinook/connector/chinook/native_queries/artists_with_albums_and_tracks.json b/fixtures/hasura/chinook/connector/native_queries/artists_with_albums_and_tracks.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/native_queries/artists_with_albums_and_tracks.json rename to fixtures/hasura/chinook/connector/native_queries/artists_with_albums_and_tracks.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Album.json b/fixtures/hasura/chinook/connector/schema/Album.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Album.json rename to fixtures/hasura/chinook/connector/schema/Album.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Artist.json b/fixtures/hasura/chinook/connector/schema/Artist.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Artist.json rename to fixtures/hasura/chinook/connector/schema/Artist.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Customer.json b/fixtures/hasura/chinook/connector/schema/Customer.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Customer.json rename to fixtures/hasura/chinook/connector/schema/Customer.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Employee.json b/fixtures/hasura/chinook/connector/schema/Employee.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Employee.json rename to fixtures/hasura/chinook/connector/schema/Employee.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Genre.json b/fixtures/hasura/chinook/connector/schema/Genre.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Genre.json rename to fixtures/hasura/chinook/connector/schema/Genre.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Invoice.json b/fixtures/hasura/chinook/connector/schema/Invoice.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Invoice.json rename to fixtures/hasura/chinook/connector/schema/Invoice.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/InvoiceLine.json b/fixtures/hasura/chinook/connector/schema/InvoiceLine.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/InvoiceLine.json rename to fixtures/hasura/chinook/connector/schema/InvoiceLine.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/MediaType.json b/fixtures/hasura/chinook/connector/schema/MediaType.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/MediaType.json rename to fixtures/hasura/chinook/connector/schema/MediaType.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Playlist.json b/fixtures/hasura/chinook/connector/schema/Playlist.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Playlist.json rename to fixtures/hasura/chinook/connector/schema/Playlist.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/PlaylistTrack.json b/fixtures/hasura/chinook/connector/schema/PlaylistTrack.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/PlaylistTrack.json rename to fixtures/hasura/chinook/connector/schema/PlaylistTrack.json diff --git a/fixtures/hasura/chinook/connector/chinook/schema/Track.json b/fixtures/hasura/chinook/connector/schema/Track.json similarity index 100% rename from fixtures/hasura/chinook/connector/chinook/schema/Track.json rename to fixtures/hasura/chinook/connector/schema/Track.json diff --git a/fixtures/hasura/common/metadata/scalar-types/Date.hml b/fixtures/hasura/common/metadata/scalar-types/Date.hml index 62085c8c..6c8c0986 100644 --- a/fixtures/hasura/common/metadata/scalar-types/Date.hml +++ b/fixtures/hasura/common/metadata/scalar-types/Date.hml @@ -22,6 +22,14 @@ definition: dataConnectorScalarType: Date representation: Date +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Date + representation: Date + --- kind: BooleanExpressionType version: v1 @@ -62,6 +70,15 @@ definition: _gte: _gte _lt: _lt _lte: _lte + - dataConnectorName: test_cases + dataConnectorScalarType: Date + operatorMapping: + _eq: _eq + _neq: _neq + _gt: _gt + _gte: _gte + _lt: _lt + _lte: _lte logicalOperators: enable: true isNull: @@ -93,6 +110,11 @@ definition: functionMapping: _max: { name: max } _min: { name: min } + - dataConnectorName: test_cases + dataConnectorScalarType: Date + functionMapping: + _max: { name: max } + _min: { name: min } count: { enable: true } countDistinct: { enable: true } graphql: diff --git a/fixtures/hasura/common/metadata/scalar-types/Decimal.hml b/fixtures/hasura/common/metadata/scalar-types/Decimal.hml index 1b1eb061..55211607 100644 --- a/fixtures/hasura/common/metadata/scalar-types/Decimal.hml +++ b/fixtures/hasura/common/metadata/scalar-types/Decimal.hml @@ -22,6 +22,14 @@ definition: dataConnectorScalarType: Decimal representation: Decimal +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Decimal + representation: Decimal + --- kind: BooleanExpressionType version: v1 @@ -62,6 +70,15 @@ definition: _gte: _gte _lt: _lt _lte: _lte + - dataConnectorName: test_cases + dataConnectorScalarType: Decimal + operatorMapping: + _eq: _eq + _neq: _neq + _gt: _gt + _gte: _gte + _lt: _lt + _lte: _lte logicalOperators: enable: true isNull: @@ -101,6 +118,13 @@ definition: _max: { name: max } _min: { name: min } _sum: { name: sum } + - dataConnectorName: test_cases + dataConnectorScalarType: Decimal + functionMapping: + _avg: { name: avg } + _max: { name: max } + _min: { name: min } + _sum: { name: sum } count: { enable: true } countDistinct: { enable: true } graphql: diff --git a/fixtures/hasura/common/metadata/scalar-types/Double.hml b/fixtures/hasura/common/metadata/scalar-types/Double.hml index 7d4af850..e91ca3d4 100644 --- a/fixtures/hasura/common/metadata/scalar-types/Double.hml +++ b/fixtures/hasura/common/metadata/scalar-types/Double.hml @@ -14,6 +14,14 @@ definition: dataConnectorScalarType: Double representation: Float +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Double + representation: Float + --- kind: BooleanExpressionType version: v1 @@ -54,6 +62,15 @@ definition: _gte: _gte _lt: _lt _lte: _lte + - dataConnectorName: test_cases + dataConnectorScalarType: Double + operatorMapping: + _eq: _eq + _neq: _neq + _gt: _gt + _gte: _gte + _lt: _lt + _lte: _lte logicalOperators: enable: true isNull: @@ -93,6 +110,13 @@ definition: _max: { name: max } _min: { name: min } _sum: { name: sum } + - dataConnectorName: test_cases + dataConnectorScalarType: Double + functionMapping: + _avg: { name: avg } + _max: { name: max } + _min: { name: min } + _sum: { name: sum } count: { enable: true } countDistinct: { enable: true } graphql: diff --git a/fixtures/hasura/common/metadata/scalar-types/ExtendedJSON.hml b/fixtures/hasura/common/metadata/scalar-types/ExtendedJSON.hml index 000dfda6..5d6fae4c 100644 --- a/fixtures/hasura/common/metadata/scalar-types/ExtendedJSON.hml +++ b/fixtures/hasura/common/metadata/scalar-types/ExtendedJSON.hml @@ -22,6 +22,14 @@ definition: dataConnectorScalarType: ExtendedJSON representation: ExtendedJSON +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: ExtendedJSON + representation: ExtendedJSON + --- kind: BooleanExpressionType version: v1 @@ -70,6 +78,17 @@ definition: _lte: _lte _regex: _regex _iregex: _iregex + - dataConnectorName: test_cases + dataConnectorScalarType: ExtendedJSON + operatorMapping: + _eq: _eq + _neq: _neq + _gt: _gt + _gte: _gte + _lt: _lt + _lte: _lte + _regex: _regex + _iregex: _iregex logicalOperators: enable: true isNull: @@ -109,6 +128,13 @@ definition: _max: { name: max } _min: { name: min } _sum: { name: sum } + - dataConnectorName: test_cases + dataConnectorScalarType: ExtendedJSON + functionMapping: + _avg: { name: avg } + _max: { name: max } + _min: { name: min } + _sum: { name: sum } count: { enable: true } countDistinct: { enable: true } graphql: diff --git a/fixtures/hasura/common/metadata/scalar-types/Int.hml b/fixtures/hasura/common/metadata/scalar-types/Int.hml index d5d7b0bd..f1098686 100644 --- a/fixtures/hasura/common/metadata/scalar-types/Int.hml +++ b/fixtures/hasura/common/metadata/scalar-types/Int.hml @@ -14,6 +14,14 @@ definition: dataConnectorScalarType: Int representation: Int +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Int + representation: Int + --- kind: BooleanExpressionType version: v1 @@ -54,6 +62,15 @@ definition: _gte: _gte _lt: _lt _lte: _lte + - dataConnectorName: test_cases + dataConnectorScalarType: Int + operatorMapping: + _eq: _eq + _neq: _neq + _gt: _gt + _gte: _gte + _lt: _lt + _lte: _lte logicalOperators: enable: true isNull: @@ -93,6 +110,13 @@ definition: _max: { name: max } _min: { name: min } _sum: { name: sum } + - dataConnectorName: test_cases + dataConnectorScalarType: Int + functionMapping: + _avg: { name: avg } + _max: { name: max } + _min: { name: min } + _sum: { name: sum } count: { enable: true } countDistinct: { enable: true } graphql: diff --git a/fixtures/hasura/common/metadata/scalar-types/ObjectId.hml b/fixtures/hasura/common/metadata/scalar-types/ObjectId.hml index d89d0ca8..fbf46cad 100644 --- a/fixtures/hasura/common/metadata/scalar-types/ObjectId.hml +++ b/fixtures/hasura/common/metadata/scalar-types/ObjectId.hml @@ -22,6 +22,14 @@ definition: dataConnectorScalarType: ObjectId representation: ObjectId +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: ObjectId + representation: ObjectId + --- kind: BooleanExpressionType version: v1 @@ -46,6 +54,11 @@ definition: operatorMapping: _eq: _eq _neq: _neq + - dataConnectorName: test_cases + dataConnectorScalarType: ObjectId + operatorMapping: + _eq: _eq + _neq: _neq logicalOperators: enable: true isNull: diff --git a/fixtures/hasura/common/metadata/scalar-types/String.hml b/fixtures/hasura/common/metadata/scalar-types/String.hml index fb03feb4..51efea15 100644 --- a/fixtures/hasura/common/metadata/scalar-types/String.hml +++ b/fixtures/hasura/common/metadata/scalar-types/String.hml @@ -14,6 +14,14 @@ definition: dataConnectorScalarType: String representation: String +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: String + representation: String + --- kind: BooleanExpressionType version: v1 @@ -62,6 +70,17 @@ definition: _lte: _lte _regex: _regex _iregex: _iregex + - dataConnectorName: test_cases + dataConnectorScalarType: String + operatorMapping: + _eq: _eq + _neq: _neq + _gt: _gt + _gte: _gte + _lt: _lt + _lte: _lte + _regex: _regex + _iregex: _iregex logicalOperators: enable: true isNull: diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/.configuration_metadata b/fixtures/hasura/sample_mflix/connector/.configuration_metadata similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/.configuration_metadata rename to fixtures/hasura/sample_mflix/connector/.configuration_metadata diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/.ddnignore b/fixtures/hasura/sample_mflix/connector/.ddnignore similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/.ddnignore rename to fixtures/hasura/sample_mflix/connector/.ddnignore diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/.env b/fixtures/hasura/sample_mflix/connector/.env similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/.env rename to fixtures/hasura/sample_mflix/connector/.env diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/configuration.json b/fixtures/hasura/sample_mflix/connector/configuration.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/configuration.json rename to fixtures/hasura/sample_mflix/connector/configuration.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/connector.yaml b/fixtures/hasura/sample_mflix/connector/connector.yaml similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/connector.yaml rename to fixtures/hasura/sample_mflix/connector/connector.yaml diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/native_queries/extended_json_test_data.json b/fixtures/hasura/sample_mflix/connector/native_queries/extended_json_test_data.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/native_queries/extended_json_test_data.json rename to fixtures/hasura/sample_mflix/connector/native_queries/extended_json_test_data.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/native_queries/hello.json b/fixtures/hasura/sample_mflix/connector/native_queries/hello.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/native_queries/hello.json rename to fixtures/hasura/sample_mflix/connector/native_queries/hello.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/native_queries/title_word_requency.json b/fixtures/hasura/sample_mflix/connector/native_queries/title_word_requency.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/native_queries/title_word_requency.json rename to fixtures/hasura/sample_mflix/connector/native_queries/title_word_requency.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/comments.json b/fixtures/hasura/sample_mflix/connector/schema/comments.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/schema/comments.json rename to fixtures/hasura/sample_mflix/connector/schema/comments.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/movies.json b/fixtures/hasura/sample_mflix/connector/schema/movies.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/schema/movies.json rename to fixtures/hasura/sample_mflix/connector/schema/movies.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/sessions.json b/fixtures/hasura/sample_mflix/connector/schema/sessions.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/schema/sessions.json rename to fixtures/hasura/sample_mflix/connector/schema/sessions.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/theaters.json b/fixtures/hasura/sample_mflix/connector/schema/theaters.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/schema/theaters.json rename to fixtures/hasura/sample_mflix/connector/schema/theaters.json diff --git a/fixtures/hasura/sample_mflix/connector/sample_mflix/schema/users.json b/fixtures/hasura/sample_mflix/connector/schema/users.json similarity index 100% rename from fixtures/hasura/sample_mflix/connector/sample_mflix/schema/users.json rename to fixtures/hasura/sample_mflix/connector/schema/users.json diff --git a/fixtures/hasura/test_cases/.env.test_cases b/fixtures/hasura/test_cases/.env.test_cases new file mode 100644 index 00000000..3df0caa2 --- /dev/null +++ b/fixtures/hasura/test_cases/.env.test_cases @@ -0,0 +1 @@ +TEST_CASES_CONNECTOR_URL='http://localhost:7132' diff --git a/fixtures/hasura/test_cases/connector/.configuration_metadata b/fixtures/hasura/test_cases/connector/.configuration_metadata new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/hasura/test_cases/connector/.ddnignore b/fixtures/hasura/test_cases/connector/.ddnignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/fixtures/hasura/test_cases/connector/.ddnignore @@ -0,0 +1 @@ +.env diff --git a/fixtures/hasura/test_cases/connector/.env b/fixtures/hasura/test_cases/connector/.env new file mode 100644 index 00000000..74da2101 --- /dev/null +++ b/fixtures/hasura/test_cases/connector/.env @@ -0,0 +1 @@ +MONGODB_DATABASE_URI="mongodb://localhost/test_cases" diff --git a/fixtures/hasura/test_cases/connector/configuration.json b/fixtures/hasura/test_cases/connector/configuration.json new file mode 100644 index 00000000..60693388 --- /dev/null +++ b/fixtures/hasura/test_cases/connector/configuration.json @@ -0,0 +1,10 @@ +{ + "introspectionOptions": { + "sampleSize": 100, + "noValidatorSchema": false, + "allSchemaNullable": false + }, + "serializationOptions": { + "extendedJsonMode": "relaxed" + } +} diff --git a/fixtures/hasura/test_cases/connector/connector.yaml b/fixtures/hasura/test_cases/connector/connector.yaml new file mode 100644 index 00000000..0d6604cd --- /dev/null +++ b/fixtures/hasura/test_cases/connector/connector.yaml @@ -0,0 +1,8 @@ +kind: Connector +version: v1 +definition: + name: test_cases + subgraph: test_cases + source: hasura/mongodb:v0.1.0 + context: . + envFile: .env diff --git a/fixtures/hasura/test_cases/connector/schema/nested_collection.json b/fixtures/hasura/test_cases/connector/schema/nested_collection.json new file mode 100644 index 00000000..df749f60 --- /dev/null +++ b/fixtures/hasura/test_cases/connector/schema/nested_collection.json @@ -0,0 +1,40 @@ +{ + "name": "nested_collection", + "collections": { + "nested_collection": { + "type": "nested_collection" + } + }, + "objectTypes": { + "nested_collection": { + "fields": { + "_id": { + "type": { + "scalar": "objectId" + } + }, + "institution": { + "type": { + "scalar": "string" + } + }, + "staff": { + "type": { + "arrayOf": { + "object": "nested_collection_staff" + } + } + } + } + }, + "nested_collection_staff": { + "fields": { + "name": { + "type": { + "scalar": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/hasura/test_cases/connector/schema/weird_field_names.json b/fixtures/hasura/test_cases/connector/schema/weird_field_names.json new file mode 100644 index 00000000..2fbd8940 --- /dev/null +++ b/fixtures/hasura/test_cases/connector/schema/weird_field_names.json @@ -0,0 +1,52 @@ +{ + "name": "weird_field_names", + "collections": { + "weird_field_names": { + "type": "weird_field_names" + } + }, + "objectTypes": { + "weird_field_names": { + "fields": { + "$invalid.name": { + "type": { + "scalar": "int" + } + }, + "$invalid.object.name": { + "type": { + "object": "weird_field_names_$invalid.object.name" + } + }, + "_id": { + "type": { + "scalar": "objectId" + } + }, + "valid_object_name": { + "type": { + "object": "weird_field_names_valid_object_name" + } + } + } + }, + "weird_field_names_$invalid.object.name": { + "fields": { + "valid_name": { + "type": { + "scalar": "int" + } + } + } + }, + "weird_field_names_valid_object_name": { + "fields": { + "$invalid.nested.name": { + "type": { + "scalar": "int" + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/hasura/test_cases/metadata/models/NestedCollection.hml b/fixtures/hasura/test_cases/metadata/models/NestedCollection.hml new file mode 100644 index 00000000..121fa6df --- /dev/null +++ b/fixtures/hasura/test_cases/metadata/models/NestedCollection.hml @@ -0,0 +1,150 @@ +--- +kind: ObjectType +version: v1 +definition: + name: NestedCollectionStaff + fields: + - name: name + type: String! + graphql: + typeName: TestCases_NestedCollectionStaff + inputTypeName: TestCases_NestedCollectionStaffInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: nested_collection_staff + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: NestedCollectionStaffComparisonExp + operand: + object: + type: NestedCollectionStaff + comparableFields: + - fieldName: name + booleanExpressionType: StringComparisonExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: TestCases_NestedCollectionStaffComparisonExp + + +--- +kind: TypePermissions +version: v1 +definition: + typeName: NestedCollectionStaff + permissions: + - role: admin + output: + allowedFields: + - name + +--- +kind: ObjectType +version: v1 +definition: + name: NestedCollection + fields: + - name: id + type: ObjectId! + - name: institution + type: String! + - name: staff + type: "[NestedCollectionStaff!]!" + graphql: + typeName: TestCases_NestedCollection + inputTypeName: TestCases_NestedCollectionInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: nested_collection + fieldMapping: + id: + column: + name: _id + institution: + column: + name: institution + staff: + column: + name: staff + +--- +kind: TypePermissions +version: v1 +definition: + typeName: NestedCollection + permissions: + - role: admin + output: + allowedFields: + - id + - institution + - staff + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: NestedCollectionComparisonExp + operand: + object: + type: NestedCollection + comparableFields: + - fieldName: id + booleanExpressionType: ObjectIdComparisonExp + - fieldName: institution + booleanExpressionType: StringComparisonExp + - fieldName: staff + booleanExpressionType: NestedCollectionStaffComparisonExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: TestCases_NestedCollectionComparisonExp + +--- +kind: Model +version: v1 +definition: + name: NestedCollection + objectType: NestedCollection + source: + dataConnectorName: test_cases + collection: nested_collection + filterExpressionType: NestedCollectionComparisonExp + orderableFields: + - fieldName: id + orderByDirections: + enableAll: true + - fieldName: institution + orderByDirections: + enableAll: true + - fieldName: staff + orderByDirections: + enableAll: true + graphql: + selectMany: + queryRootField: testCases_nestedCollection + selectUniques: + - queryRootField: testCases_nestedCollectionById + uniqueIdentifier: + - id + orderByExpressionType: TestCases_NestedCollectionOrderBy + +--- +kind: ModelPermissions +version: v1 +definition: + modelName: NestedCollection + permissions: + - role: admin + select: + filter: null + diff --git a/fixtures/hasura/test_cases/metadata/models/WeirdFieldNames.hml b/fixtures/hasura/test_cases/metadata/models/WeirdFieldNames.hml new file mode 100644 index 00000000..d66ced1c --- /dev/null +++ b/fixtures/hasura/test_cases/metadata/models/WeirdFieldNames.hml @@ -0,0 +1,170 @@ +--- +kind: ObjectType +version: v1 +definition: + name: WeirdFieldNamesInvalidObjectName + fields: + - name: validName + type: Int! + graphql: + typeName: TestCases_WeirdFieldNamesInvalidObjectName + inputTypeName: TestCases_WeirdFieldNamesInvalidObjectNameInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: weird_field_names_$invalid.object.name + fieldMapping: + validName: + column: + name: valid_name + +--- +kind: TypePermissions +version: v1 +definition: + typeName: WeirdFieldNamesInvalidObjectName + permissions: + - role: admin + output: + allowedFields: + - validName + +--- +kind: ObjectType +version: v1 +definition: + name: WeirdFieldNamesValidObjectName + fields: + - name: invalidNestedName + type: Int! + graphql: + typeName: TestCases_WeirdFieldNamesValidObjectName + inputTypeName: TestCases_WeirdFieldNamesValidObjectNameInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: weird_field_names_valid_object_name + fieldMapping: + invalidNestedName: + column: + name: $invalid.nested.name + +--- +kind: TypePermissions +version: v1 +definition: + typeName: WeirdFieldNamesValidObjectName + permissions: + - role: admin + output: + allowedFields: + - invalidNestedName + +--- +kind: ObjectType +version: v1 +definition: + name: WeirdFieldNames + fields: + - name: invalidName + type: Int! + - name: invalidObjectName + type: WeirdFieldNamesInvalidObjectName! + - name: id + type: ObjectId! + - name: validObjectName + type: WeirdFieldNamesValidObjectName! + graphql: + typeName: TestCases_WeirdFieldNames + inputTypeName: TestCases_WeirdFieldNamesInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: weird_field_names + fieldMapping: + invalidName: + column: + name: $invalid.name + invalidObjectName: + column: + name: $invalid.object.name + id: + column: + name: _id + validObjectName: + column: + name: valid_object_name + +--- +kind: TypePermissions +version: v1 +definition: + typeName: WeirdFieldNames + permissions: + - role: admin + output: + allowedFields: + - invalidName + - invalidObjectName + - id + - validObjectName + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: WeirdFieldNamesComparisonExp + operand: + object: + type: WeirdFieldNames + comparableFields: + - fieldName: invalidName + booleanExpressionType: IntComparisonExp + - fieldName: id + booleanExpressionType: ObjectIdComparisonExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: TestCases_WeirdFieldNamesComparisonExp + +--- +kind: Model +version: v1 +definition: + name: WeirdFieldNames + objectType: WeirdFieldNames + source: + dataConnectorName: test_cases + collection: weird_field_names + filterExpressionType: WeirdFieldNamesComparisonExp + orderableFields: + - fieldName: invalidName + orderByDirections: + enableAll: true + - fieldName: invalidObjectName + orderByDirections: + enableAll: true + - fieldName: id + orderByDirections: + enableAll: true + - fieldName: validObjectName + orderByDirections: + enableAll: true + graphql: + selectMany: + queryRootField: testCases_weirdFieldNames + selectUniques: + - queryRootField: testCases_weirdFieldNamesById + uniqueIdentifier: + - id + orderByExpressionType: TestCases_WeirdFieldNamesOrderBy + +--- +kind: ModelPermissions +version: v1 +definition: + modelName: WeirdFieldNames + permissions: + - role: admin + select: + filter: null diff --git a/fixtures/hasura/test_cases/metadata/test_cases.hml b/fixtures/hasura/test_cases/metadata/test_cases.hml new file mode 100644 index 00000000..932b3a2b --- /dev/null +++ b/fixtures/hasura/test_cases/metadata/test_cases.hml @@ -0,0 +1,660 @@ +kind: DataConnectorLink +version: v1 +definition: + name: test_cases + url: + readWriteUrls: + read: + valueFromEnv: TEST_CASES_CONNECTOR_URL + write: + valueFromEnv: TEST_CASES_CONNECTOR_URL + schema: + version: v0.1 + schema: + scalar_types: + BinData: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: BinData + Bool: + representation: + type: boolean + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: Bool + Date: + representation: + type: timestamp + aggregate_functions: + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: Date + min: + result_type: + type: named + name: Date + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: Date + _gte: + type: custom + argument_type: + type: named + name: Date + _lt: + type: custom + argument_type: + type: named + name: Date + _lte: + type: custom + argument_type: + type: named + name: Date + _neq: + type: custom + argument_type: + type: named + name: Date + DbPointer: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: DbPointer + Decimal: + representation: + type: bigdecimal + aggregate_functions: + avg: + result_type: + type: named + name: Decimal + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: Decimal + min: + result_type: + type: named + name: Decimal + sum: + result_type: + type: named + name: Decimal + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: Decimal + _gte: + type: custom + argument_type: + type: named + name: Decimal + _lt: + type: custom + argument_type: + type: named + name: Decimal + _lte: + type: custom + argument_type: + type: named + name: Decimal + _neq: + type: custom + argument_type: + type: named + name: Decimal + Double: + representation: + type: float64 + aggregate_functions: + avg: + result_type: + type: named + name: Double + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: Double + min: + result_type: + type: named + name: Double + sum: + result_type: + type: named + name: Double + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: Double + _gte: + type: custom + argument_type: + type: named + name: Double + _lt: + type: custom + argument_type: + type: named + name: Double + _lte: + type: custom + argument_type: + type: named + name: Double + _neq: + type: custom + argument_type: + type: named + name: Double + ExtendedJSON: + representation: + type: json + aggregate_functions: + avg: + result_type: + type: named + name: ExtendedJSON + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: ExtendedJSON + min: + result_type: + type: named + name: ExtendedJSON + sum: + result_type: + type: named + name: ExtendedJSON + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: ExtendedJSON + _gte: + type: custom + argument_type: + type: named + name: ExtendedJSON + _iregex: + type: custom + argument_type: + type: named + name: String + _lt: + type: custom + argument_type: + type: named + name: ExtendedJSON + _lte: + type: custom + argument_type: + type: named + name: ExtendedJSON + _neq: + type: custom + argument_type: + type: named + name: ExtendedJSON + _regex: + type: custom + argument_type: + type: named + name: String + Int: + representation: + type: int32 + aggregate_functions: + avg: + result_type: + type: named + name: Int + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: Int + min: + result_type: + type: named + name: Int + sum: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: Int + _gte: + type: custom + argument_type: + type: named + name: Int + _lt: + type: custom + argument_type: + type: named + name: Int + _lte: + type: custom + argument_type: + type: named + name: Int + _neq: + type: custom + argument_type: + type: named + name: Int + Javascript: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: {} + JavascriptWithScope: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: {} + Long: + representation: + type: int64 + aggregate_functions: + avg: + result_type: + type: named + name: Long + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: Long + min: + result_type: + type: named + name: Long + sum: + result_type: + type: named + name: Long + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: Long + _gte: + type: custom + argument_type: + type: named + name: Long + _lt: + type: custom + argument_type: + type: named + name: Long + _lte: + type: custom + argument_type: + type: named + name: Long + _neq: + type: custom + argument_type: + type: named + name: Long + MaxKey: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: MaxKey + MinKey: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: MinKey + "Null": + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: "Null" + ObjectId: + representation: + type: string + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: ObjectId + Regex: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: {} + String: + representation: + type: string + aggregate_functions: + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: String + min: + result_type: + type: named + name: String + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: String + _gte: + type: custom + argument_type: + type: named + name: String + _iregex: + type: custom + argument_type: + type: named + name: String + _lt: + type: custom + argument_type: + type: named + name: String + _lte: + type: custom + argument_type: + type: named + name: String + _neq: + type: custom + argument_type: + type: named + name: String + _regex: + type: custom + argument_type: + type: named + name: String + Symbol: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: Symbol + Timestamp: + aggregate_functions: + count: + result_type: + type: named + name: Int + max: + result_type: + type: named + name: Timestamp + min: + result_type: + type: named + name: Timestamp + comparison_operators: + _eq: + type: equal + _gt: + type: custom + argument_type: + type: named + name: Timestamp + _gte: + type: custom + argument_type: + type: named + name: Timestamp + _lt: + type: custom + argument_type: + type: named + name: Timestamp + _lte: + type: custom + argument_type: + type: named + name: Timestamp + _neq: + type: custom + argument_type: + type: named + name: Timestamp + Undefined: + aggregate_functions: + count: + result_type: + type: named + name: Int + comparison_operators: + _eq: + type: equal + _neq: + type: custom + argument_type: + type: named + name: Undefined + object_types: + nested_collection: + fields: + _id: + type: + type: named + name: ObjectId + institution: + type: + type: named + name: String + staff: + type: + type: array + element_type: + type: named + name: nested_collection_staff + nested_collection_staff: + fields: + name: + type: + type: named + name: String + weird_field_names: + fields: + $invalid.name: + type: + type: named + name: Int + $invalid.object.name: + type: + type: named + name: weird_field_names_$invalid.object.name + _id: + type: + type: named + name: ObjectId + valid_object_name: + type: + type: named + name: weird_field_names_valid_object_name + weird_field_names_$invalid.object.name: + fields: + valid_name: + type: + type: named + name: Int + weird_field_names_valid_object_name: + fields: + $invalid.nested.name: + type: + type: named + name: Int + collections: + - name: nested_collection + arguments: {} + type: nested_collection + uniqueness_constraints: + nested_collection_id: + unique_columns: + - _id + foreign_keys: {} + - name: weird_field_names + arguments: {} + type: weird_field_names + uniqueness_constraints: + weird_field_names_id: + unique_columns: + - _id + foreign_keys: {} + functions: [] + procedures: [] + capabilities: + version: 0.1.6 + capabilities: + query: + aggregates: {} + variables: {} + explain: {} + nested_fields: + filter_by: {} + order_by: {} + mutation: {} + relationships: + relation_comparisons: {} diff --git a/fixtures/hasura/test_cases/subgraph.yaml b/fixtures/hasura/test_cases/subgraph.yaml new file mode 100644 index 00000000..12f327a9 --- /dev/null +++ b/fixtures/hasura/test_cases/subgraph.yaml @@ -0,0 +1,8 @@ +kind: Subgraph +version: v2 +definition: + generator: + rootPath: . + includePaths: + - metadata + name: test_cases diff --git a/fixtures/mongodb/sample_claims/import.sh b/fixtures/mongodb/sample_claims/import.sh new file mode 100755 index 00000000..f9b5e25c --- /dev/null +++ b/fixtures/mongodb/sample_claims/import.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +# Get the directory of this script file +FIXTURES=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# In v6 and later the bundled MongoDB client shell is called "mongosh". In +# earlier versions it's called "mongo". +MONGO_SH=mongosh +if ! command -v mongosh &> /dev/null; then + MONGO_SH=mongo +fi + +echo "📡 Importing claims sample data..." +mongoimport --db sample_claims --collection companies --type csv --headerline --file "$FIXTURES"/companies.csv +mongoimport --db sample_claims --collection carriers --type csv --headerline --file "$FIXTURES"/carriers.csv +mongoimport --db sample_claims --collection account_groups --type csv --headerline --file "$FIXTURES"/account_groups.csv +mongoimport --db sample_claims --collection claims --type csv --headerline --file "$FIXTURES"/claims.csv +$MONGO_SH sample_claims "$FIXTURES"/view_flat.js +$MONGO_SH sample_claims "$FIXTURES"/view_nested.js +echo "✅ Sample claims data imported..." diff --git a/fixtures/mongodb/sample_import.sh b/fixtures/mongodb/sample_import.sh index 21340366..1a9f8b9f 100755 --- a/fixtures/mongodb/sample_import.sh +++ b/fixtures/mongodb/sample_import.sh @@ -8,32 +8,7 @@ set -euo pipefail # Get the directory of this script file FIXTURES=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# In v6 and later the bundled MongoDB client shell is called "mongosh". In -# earlier versions it's called "mongo". -MONGO_SH=mongosh -if ! command -v mongosh &> /dev/null; then - MONGO_SH=mongo -fi - -# Sample Claims Data -echo "📡 Importing claims sample data..." -mongoimport --db sample_claims --collection companies --type csv --headerline --file "$FIXTURES"/sample_claims/companies.csv -mongoimport --db sample_claims --collection carriers --type csv --headerline --file "$FIXTURES"/sample_claims/carriers.csv -mongoimport --db sample_claims --collection account_groups --type csv --headerline --file "$FIXTURES"/sample_claims/account_groups.csv -mongoimport --db sample_claims --collection claims --type csv --headerline --file "$FIXTURES"/sample_claims/claims.csv -$MONGO_SH sample_claims "$FIXTURES"/sample_claims/view_flat.js -$MONGO_SH sample_claims "$FIXTURES"/sample_claims/view_nested.js -echo "✅ Sample claims data imported..." - -# mongo_flix -echo "📡 Importing mflix sample data..." -mongoimport --db sample_mflix --collection comments --file "$FIXTURES"/sample_mflix/comments.json -mongoimport --db sample_mflix --collection movies --file "$FIXTURES"/sample_mflix/movies.json -mongoimport --db sample_mflix --collection sessions --file "$FIXTURES"/sample_mflix/sessions.json -mongoimport --db sample_mflix --collection theaters --file "$FIXTURES"/sample_mflix/theaters.json -mongoimport --db sample_mflix --collection users --file "$FIXTURES"/sample_mflix/users.json -$MONGO_SH sample_mflix "$FIXTURES/sample_mflix/indexes.js" -echo "✅ Mflix sample data imported..." - -# chinook +"$FIXTURES"/sample_claims/import.sh +"$FIXTURES"/sample_mflix/import.sh "$FIXTURES"/chinook/chinook-import.sh +"$FIXTURES"/test_cases/import.sh diff --git a/fixtures/mongodb/sample_mflix/import.sh b/fixtures/mongodb/sample_mflix/import.sh new file mode 100755 index 00000000..d1329dae --- /dev/null +++ b/fixtures/mongodb/sample_mflix/import.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +# Get the directory of this script file +FIXTURES=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# In v6 and later the bundled MongoDB client shell is called "mongosh". In +# earlier versions it's called "mongo". +MONGO_SH=mongosh +if ! command -v mongosh &> /dev/null; then + MONGO_SH=mongo +fi + +echo "📡 Importing mflix sample data..." +mongoimport --db sample_mflix --collection comments --file "$FIXTURES"/comments.json +mongoimport --db sample_mflix --collection movies --file "$FIXTURES"/movies.json +mongoimport --db sample_mflix --collection sessions --file "$FIXTURES"/sessions.json +mongoimport --db sample_mflix --collection theaters --file "$FIXTURES"/theaters.json +mongoimport --db sample_mflix --collection users --file "$FIXTURES"/users.json +$MONGO_SH sample_mflix "$FIXTURES/indexes.js" +echo "✅ Mflix sample data imported..." diff --git a/fixtures/mongodb/test_cases/import.sh b/fixtures/mongodb/test_cases/import.sh new file mode 100755 index 00000000..37155bde --- /dev/null +++ b/fixtures/mongodb/test_cases/import.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Populates the test_cases mongodb database. When writing integration tests we +# come up against cases where we want some specific data to test against that +# doesn't exist in the sample_mflix or chinook databases. Such data can go into +# the test_cases database as needed. + +set -euo pipefail + +# Get the directory of this script file +FIXTURES=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +echo "📡 Importing test case data..." +mongoimport --db test_cases --collection weird_field_names --file "$FIXTURES"/weird_field_names.json +mongoimport --db test_cases --collection nested_collection --file "$FIXTURES"/nested_collection.json +echo "✅ test case data imported..." + diff --git a/fixtures/mongodb/test_cases/nested_collection.json b/fixtures/mongodb/test_cases/nested_collection.json new file mode 100644 index 00000000..f03fe46f --- /dev/null +++ b/fixtures/mongodb/test_cases/nested_collection.json @@ -0,0 +1,3 @@ +{ "institution": "Black Mesa", "staff": [{ "name": "Freeman" }, { "name": "Calhoun" }] } +{ "institution": "Aperture Science", "staff": [{ "name": "GLaDOS" }, { "name": "Chell" }] } +{ "institution": "City 17", "staff": [{ "name": "Alyx" }, { "name": "Freeman" }, { "name": "Breen" }] } diff --git a/fixtures/mongodb/test_cases/weird_field_names.json b/fixtures/mongodb/test_cases/weird_field_names.json new file mode 100644 index 00000000..3894de91 --- /dev/null +++ b/fixtures/mongodb/test_cases/weird_field_names.json @@ -0,0 +1,4 @@ +{ "_id": { "$oid": "66cf91a0ec1dfb55954378bd" }, "$invalid.name": 1, "$invalid.object.name": { "valid_name": 1 }, "valid_object_name": { "$invalid.nested.name": 1 } } +{ "_id": { "$oid": "66cf9230ec1dfb55954378be" }, "$invalid.name": 2, "$invalid.object.name": { "valid_name": 2 }, "valid_object_name": { "$invalid.nested.name": 2 } } +{ "_id": { "$oid": "66cf9274ec1dfb55954378bf" }, "$invalid.name": 3, "$invalid.object.name": { "valid_name": 3 }, "valid_object_name": { "$invalid.nested.name": 3 } } +{ "_id": { "$oid": "66cf9295ec1dfb55954378c0" }, "$invalid.name": 4, "$invalid.object.name": { "valid_name": 4 }, "valid_object_name": { "$invalid.nested.name": 4 } } diff --git a/flake.lock b/flake.lock index 33c900d4..7581dd31 100644 --- a/flake.lock +++ b/flake.lock @@ -137,11 +137,11 @@ "graphql-engine-source": { "flake": false, "locked": { - "lastModified": 1722615509, - "narHash": "sha256-LH10Tc/UWZ1uwxrw4tohmqR/uzVi53jHnr+ziuxJi8I=", + "lastModified": 1725482688, + "narHash": "sha256-O0lGe8SriKV1ScaZvJbpN7pLZa2nQfratOwilWZlJ38=", "owner": "hasura", "repo": "graphql-engine", - "rev": "03c85f69857ef556e9bb26f8b92e9e47317991a3", + "rev": "419ce34f5bc9aa121db055d5a548a3fb9a13956c", "type": "github" }, "original": { @@ -259,11 +259,11 @@ ] }, "locked": { - "lastModified": 1722565199, - "narHash": "sha256-2eek4vZKsYg8jip2WQWvAOGMMboQ40DIrllpsI6AlU4=", + "lastModified": 1725416653, + "narHash": "sha256-iNBv7ILlZI6ubhW0ExYy8YgiLKUerudxY7n8R5UQK2E=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a9cd2009fb2eeacfea785b45bdbbc33612bba1f1", + "rev": "e5d3f9c2f24d852cddc79716daf0f65ce8468b28", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b5c2756b..f0056bc3 100644 --- a/flake.nix +++ b/flake.nix @@ -210,6 +210,7 @@ ddn just mongosh + mongodb-cli-plugin pkg-config ]; }; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0329f46d..e1e295f7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.80.0" +channel = "1.80.1" profile = "default" # see https://rust-lang.github.io/rustup/concepts/profiles.html components = [] # see https://rust-lang.github.io/rustup/concepts/components.html