mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
add support for argument presets in commands (#340)
V3_GIT_ORIGIN_REV_ID: 9d7401e5c629040fda7824966588af7f39e4a14c
This commit is contained in:
parent
a86d2d0450
commit
c754f273eb
@ -13,7 +13,7 @@ use transitive::Transitive;
|
||||
|
||||
use crate::metadata::resolved::{ndc_validation::NDCValidationError, subgraph::Qualified};
|
||||
|
||||
use super::types::Annotation;
|
||||
use super::types::{Annotation, NamespaceAnnotation};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InternalDeveloperError {
|
||||
@ -88,6 +88,12 @@ pub enum InternalEngineError {
|
||||
#[error("unexpected annotation: {annotation}")]
|
||||
UnexpectedAnnotation { annotation: Annotation },
|
||||
|
||||
#[error("unexpected namespace annotation: {namespace_annotation:} found, expected type {expected_type:}")]
|
||||
UnexpectedNamespaceAnnotation {
|
||||
namespace_annotation: NamespaceAnnotation,
|
||||
expected_type: String,
|
||||
},
|
||||
|
||||
#[error("subscription shouldn't have been validated")]
|
||||
SubscriptionsNotSupported,
|
||||
|
||||
|
@ -1,15 +1,16 @@
|
||||
//! IR and execution logic for commands
|
||||
//!
|
||||
//! A 'command' executes a function/procedure and returns back the result of the execution.
|
||||
|
||||
use hasura_authn_core::SessionVariables;
|
||||
use lang_graphql::ast::common as ast;
|
||||
use lang_graphql::ast::common::TypeContainer;
|
||||
use lang_graphql::ast::common::TypeName;
|
||||
use lang_graphql::normalized_ast;
|
||||
use open_dds::arguments::ArgumentName;
|
||||
use open_dds::commands;
|
||||
use open_dds::commands::FunctionName;
|
||||
use open_dds::commands::ProcedureName;
|
||||
use open_dds::permissions::ValueExpression;
|
||||
use serde::Serialize;
|
||||
use serde_json as json;
|
||||
use std::collections::BTreeMap;
|
||||
@ -17,11 +18,13 @@ use std::collections::BTreeMap;
|
||||
use super::arguments;
|
||||
use super::selection_set;
|
||||
use crate::execute::error;
|
||||
use crate::execute::ir::permissions;
|
||||
use crate::execute::model_tracking::{count_command, UsagesCounts};
|
||||
use crate::metadata::resolved;
|
||||
use crate::metadata::resolved::subgraph;
|
||||
use crate::metadata::resolved::subgraph::QualifiedTypeReference;
|
||||
use crate::schema::types::CommandSourceDetail;
|
||||
use crate::schema::types::NamespaceAnnotation;
|
||||
use crate::schema::types::TypeKind;
|
||||
use crate::schema::GDS;
|
||||
|
||||
@ -86,6 +89,7 @@ pub(crate) fn generate_command_info<'n, 's>(
|
||||
session_variables: &SessionVariables,
|
||||
) -> Result<CommandInfo<'s>, error::Error> {
|
||||
let mut command_arguments = BTreeMap::new();
|
||||
|
||||
for argument in field_call.arguments.values() {
|
||||
command_arguments.extend(
|
||||
arguments::build_ndc_command_arguments_as_value(
|
||||
@ -97,6 +101,39 @@ pub(crate) fn generate_command_info<'n, 's>(
|
||||
);
|
||||
}
|
||||
|
||||
match field_call.info.namespaced {
|
||||
None => {}
|
||||
Some(NamespaceAnnotation::ArgumentPresets(preset_arguments)) => {
|
||||
// add any preset arguments from command permissions
|
||||
for (ArgumentName(argument_name_inner), (field_type, argument_value)) in
|
||||
preset_arguments
|
||||
{
|
||||
let actual_value: serde_json::Value = match argument_value {
|
||||
ValueExpression::Literal(val) => Ok(val.clone()),
|
||||
ValueExpression::SessionVariable(session_var) => {
|
||||
let value = session_variables.get(session_var).ok_or_else(|| {
|
||||
error::InternalDeveloperError::MissingSessionVariable {
|
||||
session_variable: session_var.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
permissions::typecast_session_variable(value, field_type)
|
||||
}
|
||||
}?;
|
||||
|
||||
command_arguments.insert(argument_name_inner.to_string(), actual_value);
|
||||
}
|
||||
}
|
||||
Some(other_annotation) => {
|
||||
return Err(error::Error::InternalError(error::InternalError::Engine(
|
||||
error::InternalEngineError::UnexpectedNamespaceAnnotation {
|
||||
namespace_annotation: other_annotation.clone(),
|
||||
expected_type: "ArgumentPresets".to_string(),
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// Add the name of the root command
|
||||
let mut usage_counts = UsagesCounts::new();
|
||||
count_command(command_name.clone(), &mut usage_counts);
|
||||
|
@ -32,6 +32,7 @@ pub(crate) fn get_select_filter_predicate<'s>(
|
||||
.and_then(|annotation| match annotation {
|
||||
types::NamespaceAnnotation::Filter(predicate) => Some(predicate),
|
||||
types::NamespaceAnnotation::NodeFieldTypeMappings(_) => None,
|
||||
types::NamespaceAnnotation::ArgumentPresets(_) => None,
|
||||
})
|
||||
// If we're hitting this case, it means that the caller of this
|
||||
// function expects a filter predicate, but it was not annotated
|
||||
@ -214,7 +215,7 @@ fn make_value_from_value_expression(
|
||||
}
|
||||
|
||||
/// Typecast a stringified session variable into a given type, but as a serde_json::Value
|
||||
fn typecast_session_variable(
|
||||
pub(crate) fn typecast_session_variable(
|
||||
session_var_value_wrapped: &SessionVariableValue,
|
||||
to_type: &QualifiedTypeReference,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
|
@ -15,7 +15,7 @@ use open_dds::commands::{
|
||||
self, CommandName, CommandV1, DataConnectorCommand, GraphQlRootFieldKind,
|
||||
};
|
||||
use open_dds::data_connector::DataConnectorName;
|
||||
use open_dds::permissions::{CommandPermissionsV1, Role};
|
||||
use open_dds::permissions::{CommandPermissionsV1, Role, ValueExpression};
|
||||
use open_dds::types::{BaseType, CustomTypeName, TypeName, TypeReference};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
@ -57,6 +57,7 @@ pub struct Command {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CommandPermission {
|
||||
pub allow_execution: bool,
|
||||
pub argument_presets: BTreeMap<ArgumentName, (QualifiedTypeReference, ValueExpression)>,
|
||||
}
|
||||
|
||||
fn is_valid_type(
|
||||
@ -281,13 +282,44 @@ pub fn resolve_command_source(
|
||||
}
|
||||
|
||||
pub fn resolve_command_permissions(
|
||||
command: &Command,
|
||||
permissions: &CommandPermissionsV1,
|
||||
) -> Result<HashMap<Role, CommandPermission>, Error> {
|
||||
let mut validated_permissions = HashMap::new();
|
||||
for command_permission in &permissions.permissions {
|
||||
// TODO: Use the permission predicates/presets
|
||||
let mut argument_presets = BTreeMap::new();
|
||||
|
||||
for argument_preset in &command_permission.argument_presets {
|
||||
if argument_presets.contains_key(&argument_preset.argument) {
|
||||
return Err(Error::DuplicateCommandArgumentPreset {
|
||||
command_name: command.name.clone(),
|
||||
argument_name: argument_preset.argument.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: typecheck any literal values against the argument types
|
||||
match command.arguments.get(&argument_preset.argument) {
|
||||
Some(argument) => {
|
||||
argument_presets.insert(
|
||||
argument_preset.argument.clone(),
|
||||
(
|
||||
argument.argument_type.clone(),
|
||||
argument_preset.value.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
return Err(Error::CommandArgumentPresetMismatch {
|
||||
command_name: command.name.clone(),
|
||||
argument_name: argument_preset.argument.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resolved_permission = CommandPermission {
|
||||
allow_execution: command_permission.allow_execution,
|
||||
argument_presets,
|
||||
};
|
||||
validated_permissions.insert(command_permission.role.clone(), resolved_permission);
|
||||
}
|
||||
|
@ -120,6 +120,16 @@ pub enum Error {
|
||||
command_name: Qualified<CommandName>,
|
||||
argument_name: ArgumentName,
|
||||
},
|
||||
#[error("a preset argument {argument_name:} has been set for the command {command_name:} but no such argument exists for this command")]
|
||||
CommandArgumentPresetMismatch {
|
||||
command_name: Qualified<CommandName>,
|
||||
argument_name: ArgumentName,
|
||||
},
|
||||
#[error("duplicate preset argument {argument_name:} for command {command_name:}")]
|
||||
DuplicateCommandArgumentPreset {
|
||||
command_name: Qualified<CommandName>,
|
||||
argument_name: ArgumentName,
|
||||
},
|
||||
#[error("the procedure {procedure:} in the data connector {data_connector:} for command {command_name:} has not been defined")]
|
||||
UnknownCommandProcedure {
|
||||
command_name: Qualified<CommandName>,
|
||||
|
@ -510,7 +510,7 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
|
||||
}
|
||||
})?;
|
||||
if command.permissions.is_none() {
|
||||
command.permissions = Some(resolve_command_permissions(command_permissions)?);
|
||||
command.permissions = Some(resolve_command_permissions(command, command_permissions)?);
|
||||
} else {
|
||||
return Err(Error::DuplicateCommandPermission {
|
||||
command_name: qualified_command_name.clone(),
|
||||
|
@ -165,7 +165,6 @@ pub enum ModelPredicate {
|
||||
relationship_info: PredicateRelationshipAnnotation,
|
||||
predicate: Box<ModelPredicate>,
|
||||
},
|
||||
|
||||
And(Vec<ModelPredicate>),
|
||||
Or(Vec<ModelPredicate>),
|
||||
Not(Box<ModelPredicate>),
|
||||
|
@ -2,6 +2,10 @@
|
||||
//!
|
||||
//! A 'command' executes a function/procedure and returns back the result of the execution.
|
||||
|
||||
use crate::metadata::resolved;
|
||||
use crate::schema::permissions;
|
||||
use crate::schema::types::{self, output_type::get_output_type, Annotation};
|
||||
use crate::schema::GDS;
|
||||
use lang_graphql::ast::common as ast;
|
||||
use lang_graphql::schema as gql_schema;
|
||||
use lang_graphql::schema::InputField;
|
||||
@ -12,11 +16,6 @@ use open_dds::commands::DataConnectorCommand;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::metadata::resolved;
|
||||
use crate::schema::permissions;
|
||||
use crate::schema::types::{self, output_type::get_output_type, Annotation};
|
||||
use crate::schema::GDS;
|
||||
|
||||
use super::types::output_type::get_type_kind;
|
||||
|
||||
pub enum Response {
|
||||
@ -28,6 +27,7 @@ pub enum Response {
|
||||
},
|
||||
}
|
||||
|
||||
// look at the permissions and remove arguments with presets for this role
|
||||
pub(crate) fn generate_command_argument(
|
||||
gds: &GDS,
|
||||
builder: &mut gql_schema::Builder<GDS>,
|
||||
@ -37,29 +37,47 @@ pub(crate) fn generate_command_argument(
|
||||
) -> Result<(ast::Name, Namespaced<GDS, InputField<GDS>>), crate::schema::Error> {
|
||||
let field_name = ast::Name::new(argument_name.0.as_str())?;
|
||||
let input_type = types::input_type::get_input_type(gds, builder, &argument_type.argument_type)?;
|
||||
Ok((
|
||||
|
||||
let input_field = gql_schema::InputField::new(
|
||||
field_name.clone(),
|
||||
builder.allow_all_namespaced(
|
||||
gql_schema::InputField::new(
|
||||
argument_type.description.clone(),
|
||||
Annotation::Input(types::InputAnnotation::CommandArgument {
|
||||
argument_type: argument_type.argument_type.clone(),
|
||||
ndc_func_proc_argument: command
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|command_source| command_source.argument_mappings.get(argument_name))
|
||||
.cloned(),
|
||||
}),
|
||||
input_type,
|
||||
None,
|
||||
gql_schema::DeprecationStatus::NotDeprecated,
|
||||
);
|
||||
|
||||
// a role is "allowed" to use this argument if it DOESN'T have a preset argument defined
|
||||
match &command.permissions {
|
||||
// if this command has any permissions, we must assume it has them setup for every role
|
||||
// that is interested
|
||||
Some(permissions_by_namespace) => {
|
||||
let mut namespaced_annotations = HashMap::new();
|
||||
|
||||
for (namespace, permission) in permissions_by_namespace {
|
||||
// if there is a preset for this argument, remove it from the schema
|
||||
// so the user cannot provide one
|
||||
if !permission.argument_presets.contains_key(argument_name) {
|
||||
namespaced_annotations.insert(namespace.clone(), None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
field_name,
|
||||
argument_type.description.clone(),
|
||||
Annotation::Input(types::InputAnnotation::CommandArgument {
|
||||
argument_type: argument_type.argument_type.clone(),
|
||||
ndc_func_proc_argument: command
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|command_source| {
|
||||
command_source.argument_mappings.get(argument_name)
|
||||
})
|
||||
.cloned(),
|
||||
}),
|
||||
input_type,
|
||||
None,
|
||||
gql_schema::DeprecationStatus::NotDeprecated,
|
||||
),
|
||||
None,
|
||||
),
|
||||
))
|
||||
builder.conditional_namespaced(input_field, namespaced_annotations),
|
||||
))
|
||||
}
|
||||
// if there are no permissions for this command, there are no presets so we assume all
|
||||
// arguments are OK to use
|
||||
None => Ok((field_name, builder.allow_all_namespaced(input_field, None))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn command_field(
|
||||
@ -78,6 +96,7 @@ pub(crate) fn command_field(
|
||||
let output_typename = get_output_type(gds, builder, &command.output_type)?;
|
||||
|
||||
let mut arguments = HashMap::new();
|
||||
|
||||
for (argument_name, argument_type) in &command.arguments {
|
||||
let (field_name, input_field) =
|
||||
generate_command_argument(gds, builder, command, argument_name, argument_type)?;
|
||||
@ -93,7 +112,7 @@ pub(crate) fn command_field(
|
||||
arguments,
|
||||
gql_schema::DeprecationStatus::NotDeprecated,
|
||||
),
|
||||
permissions::get_command_namespace_annotations(command),
|
||||
permissions::get_command_namespace_annotations(command)?,
|
||||
);
|
||||
Ok((command_field_name, field))
|
||||
}
|
||||
|
@ -79,19 +79,24 @@ pub(crate) fn get_model_relationship_namespace_annotations(
|
||||
/// Build namespace annotation for commands
|
||||
pub(crate) fn get_command_namespace_annotations(
|
||||
command: &resolved::command::Command,
|
||||
) -> HashMap<Role, Option<types::NamespaceAnnotation>> {
|
||||
) -> Result<HashMap<Role, Option<types::NamespaceAnnotation>>, crate::schema::Error> {
|
||||
let mut permissions = HashMap::new();
|
||||
match &command.permissions {
|
||||
Some(command_permissions) => {
|
||||
for (role, permission) in command_permissions {
|
||||
if permission.allow_execution {
|
||||
permissions.insert(role.clone(), None);
|
||||
permissions.insert(
|
||||
role.clone(),
|
||||
Some(types::NamespaceAnnotation::ArgumentPresets(
|
||||
permission.argument_presets.clone(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
permissions
|
||||
Ok(permissions)
|
||||
}
|
||||
|
||||
/// Build namespace annotation for command relationship permissions.
|
||||
@ -101,10 +106,10 @@ pub(crate) fn get_command_relationship_namespace_annotations(
|
||||
command: &resolved::command::Command,
|
||||
source_object_type_representation: &ObjectTypeRepresentation,
|
||||
mappings: &[resolved::relationship::RelationshipCommandMapping],
|
||||
) -> HashMap<Role, Option<types::NamespaceAnnotation>> {
|
||||
let select_permissions = get_command_namespace_annotations(command);
|
||||
) -> Result<HashMap<Role, Option<types::NamespaceAnnotation>>, crate::schema::Error> {
|
||||
let select_permissions = get_command_namespace_annotations(command)?;
|
||||
|
||||
select_permissions
|
||||
Ok(select_permissions
|
||||
.into_iter()
|
||||
.filter(|(role, _)| {
|
||||
mappings.iter().all(|mapping| {
|
||||
@ -115,7 +120,7 @@ pub(crate) fn get_command_relationship_namespace_annotations(
|
||||
.any(|allowed_role| role == allowed_role)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Build namespace annotations for the node interface..
|
||||
|
@ -239,6 +239,16 @@ pub enum Annotation {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
|
||||
pub enum NamespaceAnnotation {
|
||||
Filter(resolved::model::FilterPermission),
|
||||
/// any arguments that we should prefill for a command or type
|
||||
ArgumentPresets(
|
||||
BTreeMap<
|
||||
ArgumentName,
|
||||
(
|
||||
QualifiedTypeReference,
|
||||
open_dds::permissions::ValueExpression,
|
||||
),
|
||||
>,
|
||||
),
|
||||
/// The `NodeFieldTypeMappings` contains a Hashmap of typename to the filter permission.
|
||||
/// While executing the `node` field, the `id` field is supposed to be decoded and after
|
||||
/// decoding, a typename will be obtained. We need to use that typename to look up the
|
||||
|
@ -260,7 +260,7 @@ fn object_type_fields(
|
||||
command,
|
||||
object_type_representation,
|
||||
mappings,
|
||||
),
|
||||
)?,
|
||||
)
|
||||
}
|
||||
resolved::relationship::RelationshipTarget::Model {
|
||||
|
@ -120,6 +120,110 @@ pub fn test_execution_expectation_legacy(test_path_string: &str, common_metadata
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn test_introspection_expectation(
|
||||
test_path_string: &str,
|
||||
common_metadata_paths: &[&str],
|
||||
) {
|
||||
tokio_test::block_on(async {
|
||||
// Setup test context
|
||||
let root_test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
|
||||
let mut test_ctx = setup(&root_test_dir);
|
||||
let test_path = root_test_dir.join(test_path_string);
|
||||
|
||||
let request_path = test_path.join("introspection_request.gql");
|
||||
let response_path = test_path_string.to_string() + "/introspection_expected.json";
|
||||
let metadata_path = test_path.join("metadata.json");
|
||||
|
||||
let metadata_json_value = merge_with_common_metadata(
|
||||
&metadata_path,
|
||||
common_metadata_paths
|
||||
.iter()
|
||||
.map(|path| root_test_dir.join(path)),
|
||||
);
|
||||
|
||||
let metadata = open_dds::traits::OpenDd::deserialize(metadata_json_value).unwrap();
|
||||
|
||||
// TODO: remove this assert once we have stopped manually implementing Serialize for OpenDD types.
|
||||
assert_eq!(
|
||||
open_dds::Metadata::from_json_str(&serde_json::to_string(&metadata).unwrap()).unwrap(),
|
||||
metadata
|
||||
);
|
||||
|
||||
let gds = GDS::new(metadata).unwrap();
|
||||
let schema = GDS::build_schema(&gds).unwrap();
|
||||
|
||||
// Verify successful serialization and deserialization of the schema.
|
||||
// Hasura V3 relies on the serialized schema for handling requests.
|
||||
// Therefore, it is crucial to ensure the functionality of both
|
||||
// deserialization and serialization.
|
||||
// Testing this within this function allows us to detect errors for any
|
||||
// future metadata tests that may be added.
|
||||
let serialized_metadata =
|
||||
serde_json::to_string(&schema).expect("Failed to serialize schema");
|
||||
let deserialized_metadata: Schema<GDS> =
|
||||
serde_json::from_str(&serialized_metadata).expect("Failed to deserialize metadata");
|
||||
assert_eq!(
|
||||
schema, deserialized_metadata,
|
||||
"initial built metadata does not match deserialized metadata"
|
||||
);
|
||||
|
||||
let query = fs::read_to_string(request_path).unwrap();
|
||||
|
||||
let session_vars_path = &test_path.join("session_variables.json");
|
||||
let sessions: Vec<HashMap<SessionVariable, SessionVariableValue>> =
|
||||
json::from_str(fs::read_to_string(session_vars_path).unwrap().as_ref()).unwrap();
|
||||
let sessions: Vec<Session> = sessions.into_iter().map(resolve_session).collect();
|
||||
|
||||
assert!(
|
||||
sessions.len() > 1,
|
||||
"Found less than 2 roles in test scenario"
|
||||
);
|
||||
|
||||
let raw_request = RawRequest {
|
||||
operation_name: None,
|
||||
query,
|
||||
variables: None,
|
||||
};
|
||||
|
||||
// Execute the test
|
||||
let mut responses = Vec::new();
|
||||
for session in sessions.iter() {
|
||||
let response = execute_query(
|
||||
&test_ctx.http_client,
|
||||
&schema,
|
||||
session,
|
||||
raw_request.clone(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
responses.push(response.0);
|
||||
}
|
||||
|
||||
let mut expected = test_ctx
|
||||
.mint
|
||||
.new_goldenfile_with_differ(
|
||||
response_path,
|
||||
Box::new(|file1, file2| {
|
||||
let json1: serde_json::Value =
|
||||
serde_json::from_reader(File::open(file1).unwrap()).unwrap();
|
||||
let json2: serde_json::Value =
|
||||
serde_json::from_reader(File::open(file2).unwrap()).unwrap();
|
||||
if json1 != json2 {
|
||||
text_diff(file1, file2)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
write!(
|
||||
expected,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&responses).unwrap()
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn test_execution_expectation(test_path_string: &str, common_metadata_paths: &[&str]) {
|
||||
tokio_test::block_on(async {
|
||||
// Setup test context
|
||||
|
@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"getActorsByMovieIdBounds": [
|
||||
{
|
||||
"actor_id": 3,
|
||||
"movie_id": 2,
|
||||
"name": "Irfan Khan"
|
||||
},
|
||||
{
|
||||
"actor_id": 4,
|
||||
"movie_id": 3,
|
||||
"name": "Al Pacino"
|
||||
},
|
||||
{
|
||||
"actor_id": 5,
|
||||
"movie_id": 3,
|
||||
"name": "Robert De Niro"
|
||||
},
|
||||
{
|
||||
"actor_id": 6,
|
||||
"movie_id": 4,
|
||||
"name": "Morgan Freeman"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"getActorsByMovieIdBounds": [
|
||||
{
|
||||
"actor_id": 6,
|
||||
"movie_id": 4,
|
||||
"name": "Morgan Freeman"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": null,
|
||||
"errors": [
|
||||
{
|
||||
"message": "validation failed: required argument lower_bound not found on field getActorsByMovieIdBounds of type Query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"__schema": {
|
||||
"queryType": {
|
||||
"name": "Query",
|
||||
"fields": [
|
||||
{
|
||||
"name": "getActorsByMovieIdBounds",
|
||||
"args": [
|
||||
{
|
||||
"name": "upper_bound",
|
||||
"defaultValue": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"__schema": {
|
||||
"queryType": {
|
||||
"name": "Query",
|
||||
"fields": [
|
||||
{
|
||||
"name": "getActorsByMovieIdBounds",
|
||||
"args": [
|
||||
{
|
||||
"name": "upper_bound",
|
||||
"defaultValue": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"__schema": {
|
||||
"queryType": {
|
||||
"name": "Query",
|
||||
"fields": [
|
||||
{
|
||||
"name": "getActorsByMovieIdBounds",
|
||||
"args": [
|
||||
{
|
||||
"name": "lower_bound",
|
||||
"defaultValue": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upper_bound",
|
||||
"defaultValue": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
query {
|
||||
__schema {
|
||||
queryType {
|
||||
name
|
||||
fields {
|
||||
name
|
||||
args {
|
||||
name
|
||||
defaultValue
|
||||
type {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
{
|
||||
"version": "v2",
|
||||
"subgraphs": [
|
||||
{
|
||||
"name": "default",
|
||||
"objects": [
|
||||
{
|
||||
"kind": "TypePermissions",
|
||||
"version": "v1",
|
||||
"definition": {
|
||||
"typeName": "commandActor",
|
||||
"permissions": [
|
||||
{
|
||||
"role": "admin",
|
||||
"output": {
|
||||
"allowedFields": [
|
||||
"actor_id",
|
||||
"name",
|
||||
"movie_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "user_1",
|
||||
"output": {
|
||||
"allowedFields": [
|
||||
"actor_id",
|
||||
"name",
|
||||
"movie_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "user_2",
|
||||
"output": {
|
||||
"allowedFields": [
|
||||
"actor_id",
|
||||
"name",
|
||||
"movie_id"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "CommandPermissions",
|
||||
"version": "v1",
|
||||
"definition": {
|
||||
"commandName": "get_actors_by_movie_id_bounds",
|
||||
"permissions": [
|
||||
{
|
||||
"role": "admin",
|
||||
"allowExecution": true,
|
||||
"argumentPresets": [
|
||||
{
|
||||
"argument": "lower_bound",
|
||||
"value": {
|
||||
"literal": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user_1",
|
||||
"allowExecution": true,
|
||||
"argumentPresets": [
|
||||
{
|
||||
"argument": "lower_bound",
|
||||
"value": {
|
||||
"literal": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user_2",
|
||||
"allowExecution": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Command",
|
||||
"version": "v1",
|
||||
"definition": {
|
||||
"name": "get_actors_by_movie_id_bounds",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "lower_bound",
|
||||
"type": "Int!"
|
||||
},
|
||||
{
|
||||
"name": "upper_bound",
|
||||
"type": "Int!"
|
||||
}
|
||||
],
|
||||
"outputType": "[commandActor]",
|
||||
"source": {
|
||||
"dataConnectorName": "custom",
|
||||
"dataConnectorCommand": {
|
||||
"function": "get_actors_by_movie_id_bounds"
|
||||
},
|
||||
"argumentMapping": {
|
||||
"lower_bound": "lower_bound",
|
||||
"upper_bound": "upper_bound"
|
||||
}
|
||||
},
|
||||
"graphql": {
|
||||
"rootFieldName": "getActorsByMovieIdBounds",
|
||||
"rootFieldKind": "Query"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
query MyQuery {
|
||||
getActorsByMovieIdBounds(upper_bound: 4) {
|
||||
actor_id
|
||||
movie_id
|
||||
name
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"x-hasura-role": "admin"
|
||||
},
|
||||
{
|
||||
"x-hasura-role": "user_1"
|
||||
},
|
||||
{
|
||||
"x-hasura-role": "user_2"
|
||||
}
|
||||
]
|
@ -735,6 +735,24 @@ fn test_command_procedures_multiple_arguments() {
|
||||
);
|
||||
}
|
||||
|
||||
// Tests a mutation command with preset arguments:
|
||||
// arguments: 1 arguments (taken as id and new name for an actor and returns the updated commandActor row )
|
||||
// output: object (commandActor) output type
|
||||
// permission: different permissions and preset arguments for roles: admin, user_1, user_2
|
||||
#[test]
|
||||
fn test_command_functions_preset_arguments() {
|
||||
let test_path_string = "execute/commands/functions/preset_arguments";
|
||||
let common_metadata_path_string = "execute/common_metadata/custom_connector_schema.json";
|
||||
let common_command_metadata_path_string = "execute/common_metadata/command_metadata.json";
|
||||
common::test_execution_expectation(
|
||||
test_path_string,
|
||||
&[
|
||||
common_metadata_path_string,
|
||||
common_command_metadata_path_string,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Tests using relationships in predicates
|
||||
// Array relationship
|
||||
#[test]
|
||||
|
16
v3/engine/tests/introspection.rs
Normal file
16
v3/engine/tests/introspection.rs
Normal file
@ -0,0 +1,16 @@
|
||||
#[allow(dead_code)]
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn test_introspect_command_with_preset_arguments() {
|
||||
let common_metadata_path_string = "execute/common_metadata/custom_connector_schema.json";
|
||||
let common_command_metadata_path_string = "execute/common_metadata/command_metadata.json";
|
||||
|
||||
common::test_introspection_expectation(
|
||||
"execute/commands/functions/preset_arguments/",
|
||||
&[
|
||||
common_metadata_path_string,
|
||||
common_command_metadata_path_string,
|
||||
],
|
||||
);
|
||||
}
|
@ -158,6 +158,7 @@ where
|
||||
let mut field_calls = HashMap::new();
|
||||
for (reachability, fields) in typed_fields.into_iter() {
|
||||
let cannonical_field = fields.head;
|
||||
|
||||
let arguments = normalize_arguments(
|
||||
namespace,
|
||||
schema,
|
||||
|
@ -2686,7 +2686,15 @@
|
||||
"examples": [
|
||||
{
|
||||
"role": "user",
|
||||
"allowExecution": true
|
||||
"allowExecution": true,
|
||||
"argumentPresets": [
|
||||
{
|
||||
"argument": "user_id",
|
||||
"value": {
|
||||
"session_variable": "x-hasura-user_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
@ -2706,10 +2714,78 @@
|
||||
"allowExecution": {
|
||||
"description": "Whether the command is executable by the role.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"argumentPresets": {
|
||||
"description": "Preset values for arguments for this role",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ArgumentPreset"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ArgumentPreset": {
|
||||
"$id": "https://hasura.io/jsonschemas/metadata/ArgumentPreset",
|
||||
"title": "ArgumentPreset",
|
||||
"description": "Preset value for an argument",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"argument",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"argument": {
|
||||
"description": "Argument name for preset",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ArgumentName"
|
||||
}
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"description": "Value for preset",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ValueExpression2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ValueExpression2": {
|
||||
"$id": "https://hasura.io/jsonschemas/metadata/ValueExpression2",
|
||||
"title": "ValueExpression",
|
||||
"description": "An expression which evaluates to a value that can be used in permissions.",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Literal",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"literal"
|
||||
],
|
||||
"properties": {
|
||||
"literal": true
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"title": "SessionVariable",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"sessionVariable"
|
||||
],
|
||||
"properties": {
|
||||
"sessionVariable": {
|
||||
"$ref": "#/definitions/OpenDdSessionVariable"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"OpenDdMetadataWithVersion": {
|
||||
"$id": "https://hasura.io/jsonschemas/metadata/OpenDdMetadataWithVersion",
|
||||
"title": "OpenDdMetadataWithVersion",
|
||||
|
@ -9,6 +9,8 @@ use crate::{impl_JsonSchema_with_OpenDd_for, types::TypeReference};
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
derive_more::Display,
|
||||
opendds_derive::OpenDd,
|
||||
|
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::{
|
||||
arguments::ArgumentName,
|
||||
commands::CommandName,
|
||||
models::ModelName,
|
||||
relationships::RelationshipName,
|
||||
@ -33,6 +34,16 @@ impl Role {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, Eq, PartialEq, opendds_derive::OpenDd)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Preset value for an argument
|
||||
pub struct ArgumentPreset {
|
||||
/// Argument name for preset
|
||||
pub argument: ArgumentName,
|
||||
/// Value for preset
|
||||
pub value: ValueExpression,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, Eq, PartialEq, opendds_derive::OpenDd)]
|
||||
#[serde(tag = "version", content = "definition")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -293,9 +304,11 @@ impl NullableModelPredicate {
|
||||
pub struct CommandPermission {
|
||||
/// The role for which permissions are being defined.
|
||||
pub role: Role,
|
||||
// TODO: Implement predicates and presets
|
||||
/// Whether the command is executable by the role.
|
||||
pub allow_execution: bool,
|
||||
/// Preset values for arguments for this role
|
||||
#[opendd(default, json_schema(default_exp = "serde_json::json!([])"))]
|
||||
pub argument_presets: Vec<ArgumentPreset>,
|
||||
}
|
||||
|
||||
impl CommandPermission {
|
||||
@ -303,7 +316,13 @@ impl CommandPermission {
|
||||
serde_json::json!(
|
||||
{
|
||||
"role": "user",
|
||||
"allowExecution": true
|
||||
"allowExecution": true,
|
||||
"argumentPresets": [{
|
||||
"argument": "user_id",
|
||||
"value": {
|
||||
"session_variable": "x-hasura-user_id"
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -439,7 +458,9 @@ impl ModelPredicate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, opendds_derive::OpenDd,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schemars(title = "ValueExpression")]
|
||||
/// An expression which evaluates to a value that can be used in permissions.
|
||||
|
Loading…
Reference in New Issue
Block a user