diff --git a/Cargo.lock b/Cargo.lock index cabb1cad..b752b267 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,6 +410,7 @@ dependencies = [ name = "configuration" version = "0.1.0" dependencies = [ + "anyhow", "futures", "itertools 0.12.1", "mongodb", diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index e06babca..4843459b 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -43,7 +43,7 @@ async fn update(context: &Context, args: &UpdateArgs) -> anyhow::Result<()> { introspection::sample_schema_from_db(sample_size, &context.mongo_config).await? } }; - let configuration = Configuration::from_schema(schema); + let configuration = Configuration::from_schema(schema)?; configuration::write_directory(&context.path, &configuration).await?; diff --git a/crates/configuration/Cargo.toml b/crates/configuration/Cargo.toml index bea1d0e8..8db65e2e 100644 --- a/crates/configuration/Cargo.toml +++ b/crates/configuration/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1" futures = "^0.3" itertools = "^0.12" mongodb = "2.8" diff --git a/crates/configuration/src/configuration.rs b/crates/configuration/src/configuration.rs index 3a3549f9..8a5bd1a6 100644 --- a/crates/configuration/src/configuration.rs +++ b/crates/configuration/src/configuration.rs @@ -1,9 +1,11 @@ -use std::{io, path::Path}; +use std::path::Path; +use anyhow::ensure; +use itertools::Itertools; use schemars::JsonSchema; use serde::Deserialize; -use crate::{native_queries::NativeQuery, read_directory, Schema}; +use crate::{native_queries::NativeQuery, read_directory, schema::ObjectType, Schema}; #[derive(Clone, Debug, Default, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] @@ -18,16 +20,98 @@ pub struct Configuration { } impl Configuration { - pub fn from_schema(schema: Schema) -> Self { - Self { + pub fn validate(schema: Schema, native_queries: Vec) -> anyhow::Result { + let config = Configuration { schema, - ..Default::default() + native_queries, + }; + + { + let duplicate_type_names: Vec<&str> = config + .object_types() + .map(|t| t.name.as_ref()) + .duplicates() + .collect(); + ensure!( + duplicate_type_names.is_empty(), + "configuration contains multiple definitions for these object type names: {}", + duplicate_type_names.join(", ") + ); + } + + { + let duplicate_collection_names: Vec<&str> = config + .schema + .collections + .iter() + .map(|c| c.name.as_ref()) + .duplicates() + .collect(); + ensure!( + duplicate_collection_names.is_empty(), + "configuration contains multiple definitions for these collection names: {}", + duplicate_collection_names.join(", ") + ); } + + Ok(config) + } + + pub fn from_schema(schema: Schema) -> anyhow::Result { + Self::validate(schema, Default::default()) } pub async fn parse_configuration( configuration_dir: impl AsRef + Send, - ) -> io::Result { + ) -> anyhow::Result { read_directory(configuration_dir).await } + + /// Returns object types collected from schema and native queries + pub fn object_types(&self) -> impl Iterator { + let object_types_from_schema = self.schema.object_types.iter(); + let object_types_from_native_queries = self + .native_queries + .iter() + .flat_map(|native_query| &native_query.object_types); + object_types_from_schema.chain(object_types_from_native_queries) + } +} + +#[cfg(test)] +mod tests { + use mongodb::bson::doc; + + use super::*; + use crate::{schema::Type, Schema}; + + #[test] + fn fails_with_duplicate_object_types() { + let schema = Schema { + collections: Default::default(), + object_types: vec![ObjectType { + name: "Album".to_owned(), + fields: Default::default(), + description: Default::default(), + }], + }; + let native_queries = vec![NativeQuery { + name: "hello".to_owned(), + object_types: vec![ObjectType { + name: "Album".to_owned(), + fields: Default::default(), + description: Default::default(), + }], + result_type: Type::Object("Album".to_owned()), + command: doc! { "command": 1 }, + arguments: Default::default(), + selection_criteria: Default::default(), + description: Default::default(), + mode: Default::default(), + }]; + let result = Configuration::validate(schema, native_queries); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("multiple definitions")); + assert!(error_msg.contains("Album")); + } } diff --git a/crates/configuration/src/directory.rs b/crates/configuration/src/directory.rs index f80d4b23..fd616e71 100644 --- a/crates/configuration/src/directory.rs +++ b/crates/configuration/src/directory.rs @@ -1,10 +1,8 @@ +use anyhow::{anyhow, Context as _}; use futures::stream::TryStreamExt as _; use itertools::Itertools as _; use serde::{Deserialize, Serialize}; -use std::{ - io, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; use tokio::fs; use tokio_stream::wrappers::ReadDirStream; @@ -29,7 +27,7 @@ const YAML: FileFormat = FileFormat::Yaml; /// Read configuration from a directory pub async fn read_directory( configuration_dir: impl AsRef + Send, -) -> io::Result { +) -> anyhow::Result { let dir = configuration_dir.as_ref(); let schema = parse_json_or_yaml(dir, SCHEMA_FILENAME).await?; @@ -38,16 +36,13 @@ pub async fn read_directory( .await? .unwrap_or_default(); - Ok(Configuration { - schema, - native_queries, - }) + Configuration::validate(schema, native_queries) } /// Parse all files in a directory with one of the allowed configuration extensions according to /// the given type argument. For example if `T` is `NativeQuery` this function assumes that all /// json and yaml files in the given directory should be parsed as native query configurations. -async fn read_subdir_configs(subdir: &Path) -> io::Result>> +async fn read_subdir_configs(subdir: &Path) -> anyhow::Result>> where for<'a> T: Deserialize<'a>, { @@ -57,6 +52,7 @@ where let dir_stream = ReadDirStream::new(fs::read_dir(subdir).await?); let configs = dir_stream + .map_err(|err| err.into()) .try_filter_map(|dir_entry| async move { // Permits regular files and symlinks, does not filter out symlinks to directories. let is_file = !(dir_entry.file_type().await?.is_dir()); @@ -86,7 +82,7 @@ where /// Given a base name, like "connection", looks for files of the form "connection.json", /// "connection.yaml", etc; reads the file; and parses it according to its extension. -async fn parse_json_or_yaml(configuration_dir: &Path, basename: &str) -> io::Result +async fn parse_json_or_yaml(configuration_dir: &Path, basename: &str) -> anyhow::Result where for<'a> T: Deserialize<'a>, { @@ -96,7 +92,10 @@ where /// Given a base name, like "connection", looks for files of the form "connection.json", /// "connection.yaml", etc, and returns the found path with its file format. -async fn find_file(configuration_dir: &Path, basename: &str) -> io::Result<(PathBuf, FileFormat)> { +async fn find_file( + configuration_dir: &Path, + basename: &str, +) -> anyhow::Result<(PathBuf, FileFormat)> { for (extension, format) in CONFIGURATION_EXTENSIONS { let path = configuration_dir.join(format!("{basename}.{extension}")); if fs::try_exists(&path).await? { @@ -104,30 +103,28 @@ async fn find_file(configuration_dir: &Path, basename: &str) -> io::Result<(Path } } - Err(io::Error::new( - io::ErrorKind::NotFound, - format!( - "could not find file, {:?}", - configuration_dir.join(format!( - "{basename}.{{{}}}", - CONFIGURATION_EXTENSIONS - .into_iter() - .map(|(ext, _)| ext) - .join(",") - )) - ), + Err(anyhow!( + "could not find file, {:?}", + configuration_dir.join(format!( + "{basename}.{{{}}}", + CONFIGURATION_EXTENSIONS + .into_iter() + .map(|(ext, _)| ext) + .join(",") + )) )) } -async fn parse_config_file(path: impl AsRef, format: FileFormat) -> io::Result +async fn parse_config_file(path: impl AsRef, format: FileFormat) -> anyhow::Result where for<'a> T: Deserialize<'a>, { let bytes = fs::read(path.as_ref()).await?; let value = match format { - FileFormat::Json => serde_json::from_slice(&bytes)?, + FileFormat::Json => serde_json::from_slice(&bytes) + .with_context(|| format!("error parsing {:?}", path.as_ref()))?, FileFormat::Yaml => serde_yaml::from_slice(&bytes) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?, + .with_context(|| format!("error parsing {:?}", path.as_ref()))?, }; Ok(value) } @@ -136,7 +133,7 @@ where pub async fn write_directory( configuration_dir: impl AsRef, configuration: &Configuration, -) -> io::Result<()> { +) -> anyhow::Result<()> { write_file(configuration_dir, SCHEMA_FILENAME, &configuration.schema).await } @@ -149,11 +146,13 @@ async fn write_file( configuration_dir: impl AsRef, basename: &str, value: &T, -) -> io::Result<()> +) -> anyhow::Result<()> where T: Serialize, { let path = default_file_path(configuration_dir, basename); let bytes = serde_json::to_vec_pretty(value)?; - fs::write(path, bytes).await + fs::write(path.clone(), bytes) + .await + .with_context(|| format!("error writing {:?}", path)) } diff --git a/crates/configuration/src/native_queries.rs b/crates/configuration/src/native_queries.rs index 633c9ead..6153a4bf 100644 --- a/crates/configuration/src/native_queries.rs +++ b/crates/configuration/src/native_queries.rs @@ -2,7 +2,7 @@ use mongodb::{bson, options::SelectionCriteria}; use schemars::JsonSchema; use serde::Deserialize; -use crate::schema::{ObjectField, Type}; +use crate::schema::{ObjectField, ObjectType, Type}; /// An arbitrary database command using MongoDB's runCommand API. /// See https://www.mongodb.com/docs/manual/reference/method/db.runCommand/ @@ -12,10 +12,19 @@ pub struct NativeQuery { /// Name that will be used to identify the query in your data graph pub name: String, - /// Type of data returned by the query. + /// You may define object types here to reference in `result_type`. Any types defined here will + /// be merged with the definitions in `schema.json`. This allows you to maintain hand-written + /// types for native queries without having to edit a generated `schema.json` file. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub object_types: Vec, + + /// Type of data returned by the query. You may reference object types defined in the + /// `object_types` list in this definition, or you may reference object types from + /// `schema.json`. pub result_type: Type, /// Arguments for per-query customization + #[serde(default)] pub arguments: Vec, /// Command to run expressed as a BSON document @@ -31,7 +40,11 @@ pub struct NativeQuery { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - /// Set to `readWrite` if this native query might modify data in the database. + /// Set to `readWrite` if this native query might modify data in the database. When refreshing + /// a dataconnector native queries will appear in the corresponding `DataConnectorLink` + /// definition as `functions` if they are read-only, or as `procedures` if they are read-write. + /// Functions are intended to map to GraphQL Query fields, while procedures map to Mutation + /// fields. #[serde(default)] pub mode: Mode, } diff --git a/crates/mongodb-agent-common/src/query/execute_native_query_request.rs b/crates/mongodb-agent-common/src/query/execute_native_query_request.rs new file mode 100644 index 00000000..ff603dd0 --- /dev/null +++ b/crates/mongodb-agent-common/src/query/execute_native_query_request.rs @@ -0,0 +1,31 @@ +use configuration::native_queries::NativeQuery; +use dc_api::JsonResponse; +use dc_api_types::{QueryResponse, ResponseFieldValue, RowSet}; +use mongodb::Database; + +use crate::interface_types::MongoAgentError; + +pub async fn handle_native_query_request( + native_query: NativeQuery, + database: Database, +) -> Result, MongoAgentError> { + let result = database + .run_command(native_query.command, native_query.selection_criteria) + .await?; + let result_json = + serde_json::to_value(result).map_err(|err| MongoAgentError::AdHoc(err.into()))?; + + // A function returs a single row with a single column called `__value` + // https://hasura.github.io/ndc-spec/specification/queries/functions.html + let response_row = [( + "__value".to_owned(), + ResponseFieldValue::Column(result_json), + )] + .into_iter() + .collect(); + + Ok(JsonResponse::Value(QueryResponse::Single(RowSet { + aggregates: None, + rows: Some(vec![response_row]), + }))) +} diff --git a/crates/mongodb-agent-common/src/query/mod.rs b/crates/mongodb-agent-common/src/query/mod.rs index ed0abc68..5f32bee9 100644 --- a/crates/mongodb-agent-common/src/query/mod.rs +++ b/crates/mongodb-agent-common/src/query/mod.rs @@ -1,5 +1,6 @@ mod column_ref; mod constants; +mod execute_native_query_request; mod execute_query_request; mod foreach; mod make_selector; @@ -17,7 +18,10 @@ pub use self::{ make_sort::make_sort, pipeline::{is_response_faceted, pipeline_for_non_foreach, pipeline_for_query_request}, }; -use crate::interface_types::{MongoAgentError, MongoConfig}; +use crate::{ + interface_types::{MongoAgentError, MongoConfig}, + query::execute_native_query_request::handle_native_query_request, +}; pub fn collection_name(query_request_target: &Target) -> String { query_request_target.name().join(".") @@ -29,10 +33,17 @@ pub async fn handle_query_request( ) -> Result, MongoAgentError> { tracing::debug!(?config, query_request = %serde_json::to_string(&query_request).unwrap(), "executing query"); - let collection = config - .client - .database(&config.database) - .collection::(&collection_name(&query_request.target)); + let database = config.client.database(&config.database); + + let target = &query_request.target; + if let Some(native_query) = config.native_queries.iter().find(|query| { + let target_name = target.name(); + target_name.len() == 1 && target_name[0] == query.name + }) { + return handle_native_query_request(native_query.clone(), database).await; + } + + let collection = database.collection::(&collection_name(&query_request.target)); execute_query_request(&collection, query_request).await } diff --git a/crates/mongodb-connector/src/mongo_connector.rs b/crates/mongodb-connector/src/mongo_connector.rs index e7e18b2d..f23c4338 100644 --- a/crates/mongodb-connector/src/mongo_connector.rs +++ b/crates/mongodb-connector/src/mongo_connector.rs @@ -39,7 +39,9 @@ impl Connector for MongoConnector { async fn parse_configuration( configuration_dir: impl AsRef + Send, ) -> Result { - let configuration = Configuration::parse_configuration(configuration_dir).await?; + let configuration = Configuration::parse_configuration(configuration_dir) + .await + .map_err(|err| ParseError::Other(err.into()))?; Ok(configuration) } diff --git a/crates/mongodb-connector/src/schema.rs b/crates/mongodb-connector/src/schema.rs index 5cb4a7a5..d5f265e8 100644 --- a/crates/mongodb-connector/src/schema.rs +++ b/crates/mongodb-connector/src/schema.rs @@ -12,8 +12,8 @@ pub async fn get_schema( config: &Configuration, ) -> Result { let schema = &config.schema; - let object_types = map_object_types(&schema.object_types); let collections = schema.collections.iter().map(map_collection).collect(); + let object_types = config.object_types().map(map_object_type).collect(); let functions = config .native_queries @@ -38,19 +38,14 @@ pub async fn get_schema( }) } -fn map_object_types(object_types: &[schema::ObjectType]) -> BTreeMap { - object_types - .iter() - .map(|t| { - ( - t.name.clone(), - models::ObjectType { - fields: map_field_infos(&t.fields), - description: t.description.clone(), - }, - ) - }) - .collect() +fn map_object_type(object_type: &schema::ObjectType) -> (String, models::ObjectType) { + ( + object_type.name.clone(), + models::ObjectType { + fields: map_field_infos(&object_type.fields), + description: object_type.description.clone(), + }, + ) } fn map_field_infos(fields: &[schema::ObjectField]) -> BTreeMap { diff --git a/fixtures/connector/chinook/native_queries/hello.yaml b/fixtures/connector/chinook/native_queries/hello.yaml new file mode 100644 index 00000000..36b14855 --- /dev/null +++ b/fixtures/connector/chinook/native_queries/hello.yaml @@ -0,0 +1,12 @@ +name: hello +objectTypes: + - name: HelloResult + fields: + - name: ok + type: !scalar int + - name: readOnly + type: !scalar bool + # There are more fields but you get the idea +resultType: !object HelloResult +command: + hello: 1 diff --git a/fixtures/connector/chinook/schema.json b/fixtures/connector/chinook/schema.json new file mode 100644 index 00000000..4c7ee983 --- /dev/null +++ b/fixtures/connector/chinook/schema.json @@ -0,0 +1,742 @@ +{ + "collections": [ + { + "name": "Invoice", + "type": "Invoice", + "description": null + }, + { + "name": "Track", + "type": "Track", + "description": null + }, + { + "name": "MediaType", + "type": "MediaType", + "description": null + }, + { + "name": "InvoiceLine", + "type": "InvoiceLine", + "description": null + }, + { + "name": "Employee", + "type": "Employee", + "description": null + }, + { + "name": "PlaylistTrack", + "type": "PlaylistTrack", + "description": null + }, + { + "name": "Album", + "type": "Album", + "description": null + }, + { + "name": "Genre", + "type": "Genre", + "description": null + }, + { + "name": "Artist", + "type": "Artist", + "description": null + }, + { + "name": "Playlist", + "type": "Playlist", + "description": null + }, + { + "name": "Customer", + "type": "Customer", + "description": null + } + ], + "objectTypes": [ + { + "name": "Invoice", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "BillingAddress", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "BillingCity", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "BillingCountry", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "BillingPostalCode", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "BillingState", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "CustomerId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "InvoiceDate", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "InvoiceId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Total", + "type": { + "scalar": "double" + }, + "description": null + } + ], + "description": "Object type for collection Invoice" + }, + { + "name": "Track", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "AlbumId", + "type": { + "nullable": { + "scalar": "int" + } + }, + "description": null + }, + { + "name": "Bytes", + "type": { + "nullable": { + "scalar": "int" + } + }, + "description": null + }, + { + "name": "Composer", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "GenreId", + "type": { + "nullable": { + "scalar": "int" + } + }, + "description": null + }, + { + "name": "MediaTypeId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Milliseconds", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Name", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "TrackId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "UnitPrice", + "type": { + "scalar": "double" + }, + "description": null + } + ], + "description": "Object type for collection Track" + }, + { + "name": "MediaType", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "MediaTypeId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Name", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + } + ], + "description": "Object type for collection MediaType" + }, + { + "name": "InvoiceLine", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "InvoiceId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "InvoiceLineId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Quantity", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "TrackId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "UnitPrice", + "type": { + "scalar": "double" + }, + "description": null + } + ], + "description": "Object type for collection InvoiceLine" + }, + { + "name": "Employee", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "Address", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "BirthDate", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "City", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "Country", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "Email", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "EmployeeId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Fax", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "FirstName", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "HireDate", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "LastName", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "Phone", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "PostalCode", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "ReportsTo", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "State", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "Title", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + } + ], + "description": "Object type for collection Employee" + }, + { + "name": "PlaylistTrack", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "PlaylistId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "TrackId", + "type": { + "scalar": "int" + }, + "description": null + } + ], + "description": "Object type for collection PlaylistTrack" + }, + { + "name": "Album", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "AlbumId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "ArtistId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Title", + "type": { + "scalar": "string" + }, + "description": null + } + ], + "description": "Object type for collection Album" + }, + { + "name": "Genre", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "GenreId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Name", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + } + ], + "description": "Object type for collection Genre" + }, + { + "name": "Artist", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "ArtistId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Name", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + } + ], + "description": "Object type for collection Artist" + }, + { + "name": "Playlist", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "Name", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "PlaylistId", + "type": { + "scalar": "int" + }, + "description": null + } + ], + "description": "Object type for collection Playlist" + }, + { + "name": "Customer", + "fields": [ + { + "name": "_id", + "type": { + "nullable": { + "scalar": "objectId" + } + }, + "description": null + }, + { + "name": "Address", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "City", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "Company", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "Country", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "CustomerId", + "type": { + "scalar": "int" + }, + "description": null + }, + { + "name": "Email", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "Fax", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "FirstName", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "LastName", + "type": { + "scalar": "string" + }, + "description": null + }, + { + "name": "Phone", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "PostalCode", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "State", + "type": { + "nullable": { + "scalar": "string" + } + }, + "description": null + }, + { + "name": "SupportRepId", + "type": { + "nullable": { + "scalar": "int" + } + }, + "description": null + } + ], + "description": "Object type for collection Customer" + } + ] +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema.yaml b/fixtures/connector/chinook/schema.yaml deleted file mode 100644 index bbb4a52c..00000000 --- a/fixtures/connector/chinook/schema.yaml +++ /dev/null @@ -1,15 +0,0 @@ -collections: - - name: Album - type: Album - -objectTypes: - - name: Album - fields: - - name: _id - type: !scalar objectId - - name: AlbumId - type: !scalar int - - name: ArtistId - type: !scalar int - - name: Title - type: !scalar string diff --git a/fixtures/ddn/subgraphs/chinook/commands/Hello.hml b/fixtures/ddn/subgraphs/chinook/commands/Hello.hml new file mode 100644 index 00000000..cfdebd65 --- /dev/null +++ b/fixtures/ddn/subgraphs/chinook/commands/Hello.hml @@ -0,0 +1,53 @@ +kind: Command +version: v1 +definition: + name: hello + description: Example of a read-only native query + outputType: HelloResult + arguments: [] + source: + dataConnectorName: mongodb + dataConnectorCommand: + function: hello + typeMapping: + HelloResult: + fieldMapping: + ok: { column: ok } + readOnly: { column: readOnly } + graphql: + rootFieldName: hello + rootFieldKind: Query + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: hello + permissions: + - role: admin + allowExecution: true + +--- +kind: ObjectType +version: v1 +definition: + name: HelloResult + graphql: + typeName: HelloResult + fields: + - name: ok + type: Int! + - name: readOnly + type: Boolean! + +--- +kind: TypePermissions +version: v1 +definition: + typeName: HelloResult + permissions: + - role: admin + output: + allowedFields: + - ok + - readOnly diff --git a/fixtures/ddn/subgraphs/chinook/dataconnectors/mongodb.hml b/fixtures/ddn/subgraphs/chinook/dataconnectors/mongodb.hml index 4167b898..4eb5585b 100644 --- a/fixtures/ddn/subgraphs/chinook/dataconnectors/mongodb.hml +++ b/fixtures/ddn/subgraphs/chinook/dataconnectors/mongodb.hml @@ -905,6 +905,12 @@ definition: underlying_type: type: named name: ObjectId + HelloResult: + fields: + ok: + type: { type: named, name: Int } + readOnly: + type: { type: named, name: Boolean } collections: - name: Album arguments: {} @@ -994,7 +1000,11 @@ definition: unique_columns: - _id foreign_keys: {} - functions: [] + functions: + - name: hello + result_type: { type: named, name: HelloResult } + arguments: {} + command: { hello: 1 } procedures: [] capabilities: version: ^0.1.0